Yacht Devices Новости: B&G Fastnet: создаём шлюз на Python Gateway и отлаживаем код с телефона

15.04.2024

Как создать шлюз для этого протокола на базе Python Gateway и отладить программу на борту с помощью мобильного телефона.

B&G Network

Всё началось с просьбы друга посмотреть его датчик ветра. К сожалению, плата полностью сгнила. Где-то далеко нашлась похожая за 450 долларов, но мы не были уверены, что она поместится в корпус.

Так мы начали знакомиться с протоколом B&G Fastnet. Он основан на последовательном интерфейсе со скоростью 28800 бод. Протокол закрытый, но энтузиастам удалось расшифровать формат сообщений систем H2000 и H3000.

Декодирование FastNet

Рисунок 1. Декодирование FastNet

Однако на приборах нашего друга было написано Network Quad и Network Wind. Небольшое исследование показало, что они тоже используют FastNet, но со скоростью 11000 бод. Мы написали простой код для Python Gateway, который считывает данные с шины и выводит их в консоль (см. выше). Изменив скорость, этот код можно использовать и с H2000 или 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)

Мы предложили другу купить недорогой датчик ветра NMEA 0183 и подключить его к дисплею через Python Gateway. Он не был готов к большим расходам, и мы решили подарить ему наше устройство. Рабочий код приведён ниже. Разъём NMEA 2000 используется только для питания, вход NMEA 0183 RX подключён к датчику ветра, а выход NMEA 0183 TX — к дисплею B&G Network Wind.


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)

Здесь есть нюанс. Пока Quad и Wind не соединены, всё работает. Но при их соединении возникают коллизии: устройства передают сообщения по одной шине и, чтобы не мешать друг другу, должны прослушивать её перед передачей. Свободного входа у нас нет, поэтому другу придётся отказаться от отображения истинного ветра: для его вычисления нужны данные B&G Network Quad. Впрочем, по его словам, вымпельного ветра ему достаточно.

Но мы написали и код, использующий NMEA 0183 RX для прослушивания шины. В наших испытаниях ошибок не было. Программа ниже слушает шину и передаёт в неё демонстрационные данные ветра. Её можно взять за основу для преобразователя B&G FastNet в NMEA 2000 или для передачи данных из USB в 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()

Для проверки мы отправились на лодку, и коллега подключил Python Gateway к телефону Android USB-кабелем. Телефон должен поддерживать USB OTG.

Более того, он установил на телефон бесплатную программу Micro REPL (MicroPython IDE for Android) . Она показалась ему немного сырой, но вполне работоспособной и гораздо удобнее сочетания отдельного редактора кода с терминальной программой для доступа к консоли.

Python Gateway показал себя устройством для нестандартных задач: тестовую программу легко написать и отладить буквально на ходу, даже без компьютера. А мы выражаем уважение B&G, чьи приборы работают на лодке более 20 лет и заслужили искреннюю привязанность владельца.