April 15, 2024  B&G Fastnet Protocol

How to make a gateway for this protocol using Python Gateway and debug the code on the boat using a mobile phone.

B&G Network

It all started when a friend asked us to look at his wind sensor. Unfortunately, the board was completely rotted and somewhere far away we found a similar board for $450, but we weren't sure it would fit in the case.

So we started to get acquainted with B&G Fastnet protocol. The protocol is based on a 28800 baud serial interface, and although the protocol is proprietary, amateurs were able to decode the message format used in the H2000 and H3000 systems.

FastNet decoding

Figure 1. FastNet decoding

Our friend, however, had Network Quad and Network Wind written on his displays. A little research showed that they also use the FastNet protocol, but at 11000 baud. We wrote simple code (see below) for a Python Gateway, to read data from the bus and output it to the console (see above). By changing the speed setting, this code can be used with the H2000 or H3000.

### ASCII TEXT COLORS

RED    = "\033[0;31m"
GREEN  = "\033[0;32m"
YELLOW = "\033[0;33m"
WHITE  = "\033[0;37m"

### FASTNET CONSTANTS AND UTILITY METHODS

FASTNET_FMT_BYTES = [ # total size of channel data in bytes
    0, 4, 4, 5,
    6, 0, 0, 0,
    4, 0, 0, 6,
    0, 0, 0, 0
]

FASTNET_CHANNELS = { # known data channel IDs
    0x1F: f"Sea Temperature, C",
    0x4E: f"Apparant Wind Speed, raw",
    0x50: f"Apparant Wind Speed, from nmea",
    0x51: f"Apparant Wind Angle",
    0x52: f"Apparant Wind Angle, raw",
    0x41: f"Boat Speed, knots",
    0x42: f"Boat Speed, raw",
    0x57: f"MEAS W/S knots",
    0x59: f"True Wind Angle",
    0x64: f"Average Speed, knots",
    0x65: f"Average Speed, raw",
    0x75: f"Timer",
    0x7f: f"Velocity Made Good, knots",
    0x81: f"Dead Reckoning Distance",
    0x8D: f"Battery Voltage",
    0xCD: f"Stored Log, NM",
    0xCF: f"Trip Log, NM",
    0xD3: f"Dead Reckoning Course",
}

FASTNET_DIVISORS = [1, 10, 100, 1000]

def fastnet_crc(data, size, init=0):
    crc = init
    for i in range(size):
        crc += data[i]
    crc = (0x100 - crc) & 0xFF
    return crc

def fastnet_decode_channel(data):
    global FASTNET_FMT_BYTES, FASTNET_CHANNELS, FASTNET_DIVISORS
    ch = data[0]
    fmt = data[1]
    
    div = (fmt >> 6) & 0x3
    size = (fmt >> 4) & 0x3
    fmt &= 0xF
    
    ret = FASTNET_FMT_BYTES[fmt]
    if ret <= 2:
        print(f'{RED}Unknown format: {fmt}!{WHITE}')
        return -1;
        
    if fmt == 1:
        value = data[3] | (data[2] << 8)
        if (value > 0x7FFF):
            value = value - 0x10000
    elif fmt == 8:
        value = data[3] | (data[2] << 8)
    else: # format is unknown, display raw bytes
        div = 0
        value = YELLOW + data[2:ret].hex(' ')
    
    if div > 0:
        value /= FASTNET_DIVISORS[div]
    
    name = FASTNET_CHANNELS.get(ch, None)
    if name == None:
        print(f'[{YELLOW}UNKNOWN, {ch}{WHITE}]: {value}')
    else:
        print(f'[{GREEN}{name}{WHITE}]: {value}')
    return ret

def fastnet_clbk(uart_rx, data):    
    # single callback call can contain multiple messages
    while len(data) > 7:
        dst, src, length, cmd, crc = data[:5]
        if cmd == 0x01 and fastnet_crc(data, 4) == crc:
            # message type is 'data broadcast'
            data = data[5:]
            if fastnet_crc(data, length, 0x56) == data[length]:
                led.green() # fastnet channel received successfuly
                offset = 0
                while offset < length:
                    readed = fastnet_decode_channel(data[offset:])
                    if readed < 2:
                        data = None # exit from outer loop
                        # unknown size of channel, can't continue
                        break 
                    offset += readed
        if data == None:
            break
        data = data[length+1:]

# deinitializes NMEA0183 object created in boot.py
n0183.deinit()
# sets UART settings according to Fastnet specification
uart_rx.init(baudrate=11000, bits=8, stop=2, parity=1, rx_invert=True, timeout=100)

uart_rx.rxcallback(fastnet_clbk, end=None, timeout=4, force=True)

# Dummy loop. Press Ctrl+C to exit program.
try:
    while True:
        time.sleep(1)
except:
    pass
uart_rx.rxcallback(None)

We suggested to our friend to buy an inexpensive NMEA 0183 wind sensor and connect it to the display via Python Gateway. He was not ready to spend a lot of money and we decided to give him our device as a gift. Below is the working code. The NMEA 2000 connector is used for power only, the NMEA 0183 RX port is connected to the wind sensor and the NMEA 0183 TX port is connected to the B&G Network Wind instrument display.


FASTNET_DIVISORS = [1, 10, 100, 1000]

FASTNET_FMT_BYTES = [ # total size of channel data in bytes
    0, 4, 4, 5,
    6, 0, 0, 0,
    4, 0, 0, 6,
    0, 0, 0, 0
]

FASTNET_CH_AWA = 0x51
FASTNET_CH_AWS = 0x50
FASTNET_CH_VOLTAGE = 0x8D

fastnet_header = bytearray([0xFF, 0x75, 0x14, 0x01, 0x77])
fastnet_buf = bytearray(81)
fastnet_buf_size = 0

N0183_SPEED_UNITS = {
    b'K': 1.0, # knots
    b'M': 1.943844492442284 # m/s
}

def fastnet_crc(data, size, init=0):
    crc = init
    for i in range(size):
        crc += data[i]
    crc = (0x100 - crc) & 0xFF
    return crc

def fastnet_add_channel(ch, fmt, size, divisor, value):
    global fastnet_buf, fastnet_buf_size
    global FASTNET_DIVISORS, FASTNET_FMT_BYTES
    
    if (len(fastnet_buf) - fastnet_buf_size) <= FASTNET_FMT_BYTES[fmt]:
        return -1
    
    offset = fastnet_buf_size
    fastnet_buf[offset] = ch
    fastnet_buf[offset + 1] = (fmt&0xF) | ((size&0x3) << 4) | ((divisor&0x3) << 6)
    offset += 2
    
    value = round(value * FASTNET_DIVISORS[divisor])
    
    if fmt == 1:
        if (value < 0):
            value = 0x10000 + value
        fmt = 8
    if fmt == 8:
        fastnet_buf[offset] = (value >> 8) & 0xFF
        fastnet_buf[offset + 1] = value & 0xFF
        fastnet_buf_size = offset + 2

def fastnet_flush():
    global fastnet_header, fastnet_buf, fastnet_buf_size
    
    # set message size and calculate CRCs
    fastnet_header[2] = fastnet_buf_size
    fastnet_header[4] = fastnet_crc(fastnet_header, 4)
    fastnet_buf[fastnet_buf_size] = fastnet_crc(fastnet_buf, fastnet_buf_size, 0x56)
    
    # send fastnet channels
    uart_tx.write(fastnet_header)
    uart_tx.write(fastnet_buf, 0, fastnet_buf_size + 1) # + 1 byte CRC
    fastnet_buf_size = 0
    led.green()

def n0183_receive_mwv(n0183, data):
    data = data.split(b'*')[0].rstrip().split(b',')
    print(data)
    if len(data) == 6 and data[2] == b'R' and data[5] == b'A':
        awa = float(data[1])
        aws = float(data[3]) * N0183_SPEED_UNITS.get(data[4], 1.0)
        fastnet_add_channel(FASTNET_CH_AWA, 8, 0, 0, awa)
        fastnet_add_channel(FASTNET_CH_AWS, 1, 0, 2, aws)
        fastnet_add_channel(FASTNET_CH_VOLTAGE, 8, 0, 2, ydpg.busvoltage())
        fastnet_flush()

# Removes TX port from NMEA0183 object
n0183 = NMEA0183(uart_rx, rtc=rtc)
n0183.check(False)
# Reinitializes TX port to match fastnet specification
uart_tx = pyb.UART('tx', baudrate=11000, bits=8, stop=2, parity=1)

led.state(led.RED)

n0183.rxcallback(0, n0183_receive_mwv, "$--MWV", check=False)

One nuance arises here. If the Quad and Wind are not connected, everything works fine. But if they are connected, network collisions occur. The point is that the devices send messages to one bus, and in order not to interrupt each other, the bus must be listened to before sending. Unfortunately, we have no free input, and our friend will have to say goodbye to the possibility of observing True Wind on his instrument, as his calculations require data from the B&G Network Quad. However, he claims that the Apparent Wind is enough for him.

But we have written code that uses NMEA 0183 RX to listen to the bus. In our tests there were no errors on the bus. The code below listens to the bus and sends wind demo data to it. You can use it as a basis to make a B&G FastNet to NMEA 2000 converter, or to send USB data to FastNet.

# ASCII TEXT COLORS
RED    = "\033[0;31m"
GREEN  = "\033[0;32m"
YELLOW = "\033[0;33m"
WHITE  = "\033[0;37m"

FASTNET_MAX_MSG_SIZE = 80

FASTNET_DIVISORS = [1, 10, 100, 1000]

FASTNET_FMT_BYTES = [ # total size of channel data in bytes
    0, 4, 4, 5,
    6, 0, 0, 0,
    4, 0, 0, 6,
    0, 0, 0, 0
]

def fastnet_crc(data, size, init=0):
    crc = init
    for i in range(size):
        crc += data[i]
    crc = (0x100 - crc) & 0xFF
    return crc

# ID from which YDPG-01 is sending data to Fastnet
fastnet_src = 0x70
# Fastnet header buffer (destination, source, msg size, msg type, CRC )
fastnet_header = bytearray([0xFF, fastnet_src, 0x00, 0x01, 0xC2])
# Fastnet transmit data buffer
fastnet_buf = bytearray(FASTNET_MAX_MSG_SIZE + 1) # + CRC byte
fastnet_buf_size = 0

# adds data to Fastnet transmission
def fastnet_add_channel(ch, fmt, size, divisor, value):
    global fastnet_buf, fastnet_buf_size
    global FASTNET_DIVISORS, FASTNET_FMT_BYTES
    
    ret = FASTNET_FMT_BYTES[fmt]
    if ret <= 2 or (len(fastnet_buf) - fastnet_buf_size) <= ret:
        return -1 # unknown format or not enough space in transmit buffer
    
    offset = fastnet_buf_size
    fastnet_buf[offset] = ch
    fastnet_buf[offset + 1] = (fmt&0xF) | ((size&0x3) << 4) | ((divisor&0x3) << 6)
    offset += 2
    
    value = round(value * FASTNET_DIVISORS[divisor])
    
    # allow formats 1 and 8 only
    if fmt == 1:
        if (value < 0):
            value = 0x10000 + value
        fmt = 8
    if fmt == 8:
        fastnet_buf[offset] = (value >> 8) & 0xFF
        fastnet_buf[offset + 1] = value & 0xFF
        fastnet_buf_size += ret
    return ret

# sends all added channels to Fastnet
def fastnet_flush():
    global fastnet_header, fastnet_buf, fastnet_buf_size
    
    # set message size and calculate CRCs
    fastnet_header[2] = fastnet_buf_size
    fastnet_header[4] = fastnet_crc(fastnet_header, 4)
    fastnet_buf[fastnet_buf_size] = fastnet_crc(fastnet_buf, fastnet_buf_size, 0x56)
    
    # send fastnet channels
    uart_tx.write(fastnet_header)
    uart_tx.write(fastnet_buf, 0, fastnet_buf_size + 1) # + 1 byte CRC
    fastnet_buf_size = 0
    led.green() # indicate fastnet transmission

def fastnet_add_relative_wind(awa, aws):
    fastnet_add_channel(0x51, 8, 0, 0, awa)
    fastnet_add_channel(0x50, 1, 0, 2, aws)

# deinitializes NMEA0183 object created in boot.py
n0183.deinit()
# sets UART settings according to Fastnet specification
uart_rx.init(baudrate=11000, bits=8, stop=2, parity=1, rx_invert=True, timeout=100)
uart_tx.init(baudrate=11000, bits=8, stop=2, parity=1)

led.state(led.RED)

# successful data reception flag
was_received = False

# demo AWA angles
demo_angles = [-40, 0, 40, 0]
demo_index = 0

while True:
    data = uart_rx.read(5)
    if data != None and len(data) == 5 and fastnet_crc(data, 4) == data[-1]:
        src = data[1]
        size = data[2]
        cmd = data[3]
        data = uart_rx.read(size + 1)
        
        # check that message is successfuly received and not sent from YDPG
        if src != fastnet_src and data != None and len(data) == (size + 1):
            if fastnet_crc(data, len(data)-1, 0x56) == data[-1]:
                # here we can process received messages
                was_received = True

    # wait 10 milliseconds to be sure that all messages has been received and
        # we can safely send messages    
    time.sleep(0.01) 
    if uart_rx.any() == 0 and was_received:
        was_received = False
        fastnet_add_relative_wind(demo_angles[demo_index], demo_index)
        demo_index = (demo_index + 1) % len(demo_angles)
        fastnet_flush()

To test the code, we went boating and a colleague connected the Python Gateway to his Android phone via a USB cable (the phone must support USB OTG).

Moreover, he installed the free Micro REPL (MicroPython IDE for Android) software on his phone. He found it a bit crude, but it is still quite usable and much more convenient than using a separate code editor and terminal program to access the console.

Python Gateway has shown itself as a device capable of solving atypical tasks, easily creating test code and debugging literally on the go, and even without a computer. And we extend our respect to B&G, whose devices have worked on the boat for over 20 years, and have earned the sincere affection of their owner.

Permanent link...

 

 

 April 8, 2024  Engine Gateway YDEG-04 Update

Instant fuel consumption for Volvo Penta D3 and D4, and calculated fuel rate for any engine!

Volvo Penta D2-40 fuel rate

In the new firmware version we have added the RUDDER=ON/OFF parameter to disable the rudder angle data messages. Now this data is only available for the SmartCraft protocol (Mercury and MerCruiser engines) and some engines send it with a constant value of 0.

For Volvo Penta D3 and D4 engines produced in 2003-2004 (Volcano protocol, EVC-MC, requires VOLCANO=ON setting to enable) we have managed to find instantaneous fuel consumption data thanks to our users. These are not present explicitly and require calculation. To correct the calculations we have added the parameter FUEL_RATE_MUL=0.7 (the default value, the coefficient can be from 0.001 to 65.5, 0 disables the calculation of instantaneous fuel consumption). We would be very grateful for your feedback.

The Engine Gateway also provides fuel consumption data for J1939 (Volvo Penta, Yanmar and many other), VW Marine, MEFI, BRP, Mercury and MerCruiser engines.

Unfortunately, not all engine models provide instantaneous fuel consumption data. Users have long suggested that we add simulation of this data as a function of engine speed. This makes sense: after 10 years of boat ownership, many people have only cruise speed consumption in their heads, and even that is approximate. And if the boat is rented, users may not even have this data. Of course, it would be good to take into account engine load or gear engaged, but often such data is not available on simple engines either.

Well, meet the extremely confusing parameter FUEL_RATE_FAKE=ON. Let's try to explain how to use it. Let's start with the fact that it enables simulation of instantaneous fuel consumption only for engines 0 and 1 (left and right) and uses the settings of fuel tanks 8 and 9 for this purpose (for some reasons, we decided not to create separate settings, sorry).

So we have the settings:

   ENGINE_0=0
   TANK_CAPACITY_8=6000
   FUEL_RATE_MUL=30.0
   FUEL_RATE_FAKE=ON

They mean that the left or single engine (0) has the maximum 6000 RPM (set in the TANK_CAPACITY_8 setting) and it corresponds to a maximum fuel consumption of 30 US gallons per hour (in FUEL_RATE_MUL, yes exactly in gallons, not litres as it was above).

With these settings, at 3000 RPM, the Gateway will report 15 US gallons per hour, or 56.78 litres per hour. Of course, if you set the FUEL_RATE_FAKE=OFF setting, the other settings will mean what they should mean in normal operation.

Since engines rarely have a linear relationship of fuel rate to RPM, a 12-point calibration curve can be set using the TANK_CALIBRATION_8 and TANK_CALIBRATION_9 settings. It is assumed that the values 0 and 100% do not need to be calibrated. And the setting line contain adjusted values for 4,8,12,20,30,40,50,60,60,70,70,80,90 and 95 percent.

Suppose our setup looks like this:

   TANK_CALIBRATION_8=5,7,11,19,16,37,46,54,66,78,87,91

The first number (5) is the adjusted value for 4%. In the case of FUEL_RATE_FAKE=ON, it is interpreted as follows: 4% of the maximum RPM (specified in TANK_CAPACITY_8) corresponds to 5% of the maximum fuel consumption (FUEL_RATE_MUL). The other values are interpolated accordingly. Previously, calibration settings could only contain sequentially increasing values (otherwise it did not make sense for fuel tanks), but in the current version we have removed this check, because engines may have lower fuel consumption with increasing RPM.

You can use our calibration spreadsheet for the Engine Gateway (see Picture 2 below) to calculate the calibration parameters, but we will need to enter fuel consumption instead of liquid volume and percent of maximum RPM in the "Device readings" column. We recommend this article if you are not familiar with engine performance curves.

Volvo Penta D2-40 fuel rate

Picture 1. Volvo Penta D2-40 fuel rate

Let's take the Volvo Penta D2-40F engine as an example (see Picture 1 above, we will use the middle row) or the full source document. The reference data shows a maximum consumption of 9.5 litres per hour at 3200 rpm and at maximum load. To make it easier for us to fill in the table, let's assume that the maximum engine speed is 4000 RPM and the consumption is 10 litres per hour. Then it is very easy to convert the data into percentages: 1200 RPM is 30% from 4000, and so on with 5% increments (200 RPM).

Calibration spreadsheet

Picture 2. Calibration spreasheet with Volvo Penta D2-40 data

And here's our calibration, note that we must change the capacity setting to maximum RPM when coping to the YDEG.TXT file:

   TANK_CAPACITY_8=4000
   TANK_CALIBRATION_8=1,2,4,6,9,15,24,38,60,95,98,99

All that remains is to add a maximum fuel flow rate parameter (convert 10 litres per hour to gallons):

   FUEL_RATE_MUL=2.641
   FUEL_RATE_FAKE=ON

Do you need fuel rate simulation in our other engine gateways, the Outboard Gateway or the J1708 Gateway? Are you planning to use this feature? Please send us your feedback!

The firmware update is version 1.41 and is available for download.

Permanent link...

 

 

 March 27, 2024  New product: Python Gateway

Unbelievable, but true: all the power of Python 3 can run on the same power as three standard 3mm LEDs and convert AIS messages from NMEA 0183 to NMEA 2000 in just 1.5 milliseconds. We are proud to present the most fun product for installers, developers and NMEA hackers!

Python Gateway unboxed

If you want to see how it works right away, click into this example. In ten lines of code, we've created a virtual depth sounder that uses data from a real sounder and tweaks it a bit. Why it's needed? For example, the display that came with the echo sounder broke and you can't adjust the transducer offset. With these 10 lines of code, we manage to turn our Gateway into an echo sounder type device so that we can select it in the lists of data sources on the multifunctional display and give it a description that will be visible on the MFD. Moreover, the sounder we created is NMEA 2000 compliant and handles all the necessary messages.

If you are an experienced programmer and are familiar with the CAN interface and the microcontroller you are writing the code for, it will take you a week to write and debug such code in C language. If you don't believe us, ask programmers. They will tell you that it will take at most three days, but it will take at least a week. Most likely two or even three, because to comply with the Standard, you need to implement support for more than a dozen of service messages (it is not visible in the example code), and some of them are not simple.

How do you run this code on your Python Gateway? Very simple. You connect it to the PC via USB, open the disc, open the main.py file on the disc in any text editor, and paste the example code into it. Save, reboot the device and you're done.

Thonny IDE with voltmeter code

Picture 1. Thonny IDE with voltmeter code

But even better is to download the Thonny IDE. It's a free open-source development environment that allows you to edit files directly on your device, start and stop your program, view variables, highlight syntax, etc. It's just great! And it doesn't work through the disc, but through the console. When connected via USB, Python Gateway provides two USB devices at the same time: a disc and a serial port on which the Python interpreter (REPL) is running. Thonny can read and write files, and receive data from the device via a terminal session.

Terminal session with Python Gateway

Picture 2. Terminal session with Python Gateway

But of course, you can connect to the device with a good old-fashioned terminal. Type the command can.test() and ten messages received from the CAN bus will appear on the screen. It will look just like in a hacker movie, especially if your terminal has green colored characters.

If you are not a programmer or a hacker, of course, even a simple programming language and a great library for NMEA will not make your job much easier. But, in our experience, many tasks can be solved with just a few lines of code. So if you think that our product can help you, write to us, and if the task is really a few lines, we will write you the code absolutely free of charge.

In addition to USB and CAN (NMEA 2000), the product has two hardware serial ports (NMEA 0183). More precisely, two halves of two ports, as one port is only for receiving messages and the other only for transmitting. This is so that you can connect two devices at different speeds (e.g. 4800 and 38400 baud) and organize a gateway between them. The serial port has a high voltage galvanic isolation from the USB and CAN interfaces.

The implementation of serial ports has various tricks, such as inverting lines. This allows to connect not only standard RS-422A (NMEA 0183) devices, but also different exotic things. For example, Python Gateway can read B&G FastNet and Raymarine SeaTalk 1 buses, we will publish code samples soon.

As you might already know, we have the NMEA 2000 Bridge, this is a gateway between two CAN networks with its own built-in programming language for message handling. Most of the sales of these devices come from partner companies who produce their own gateways based on them for various scenarios, ranging from a simple data filters to a custom electrical propulsion telemetry and control systems.

In the Python Gateway, we have provided encryption and source code protection from copying and viewing. At the end of April we plan to publish a guide to creating and distributing secure code, which will allow companies to create closed solutions where required to comply with commercial, copyright or non-disclosure obligations.

Our product is based on code from the popular MicroPython project, which adapts Python 3 for microcontrollers. And, of course, we cannot boast the same power and richness of libraries that personal or even single-board computers such as Raspberry Pi can offer. Nevertheless, our Gateway does an excellent job of performing the tasks for which it was created.

As an example of a complex task, we have implemented an AIS converter from NMEA 0183 to NMEA 2000. Here we need to decode binary messages transmitted in NMEA 0183 envelope, convert the data into NMEA 2000 units, and package them into NMEA 2000 messages. The messages themselves look like this in NMEA 0183:

   !AIVDM,1,1,,A,13umv<7P00Q5BUBOP@J00?w`J4Jd,0*4D
   !AIVDM,1,1,,B,144jQ301ib16`;BOc2:kuS9`H8NR,0*54
   !AIVDM,1,1,,B,15W9JJ01AL16JP@OlbjRIiu`H8O9,0*54
   !AIVDM,1,1,,A,14`bt4002i12oK6Od7J1siUbH@SH,0*72
   !AIVDM,1,1,,B,148Onl701D14k@<OlI=JupSdH8OB,0*67

It takes 1.5 milliseconds to process such a message, allowing this converter to easily operate in real time, as the 38400 baud connection can transmit less than 80 messages per second. The unit consumes 32mA only, which is equivalent to the current of three small LEDs such as those installed on the unit itself. When transmitting over NMEA 0183, the current with maximum load can rise up to 68 mA.

To start getting familiar with the product, go to its page or the Programming Manual page. Once you start using it, you will be fascinated by it, we assure you! If you have already received your device, download firmware update 1.02 from the Download section.

Permanent link...

 

Next articles:

All news...