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.

 

Next articles:

Previous articles:

See also: recent news, all news...