Yacht Devices Новости: B&G Fastnet: создаём шлюз на Python Gateway и отлаживаем код с телефона
15.04.2024
Как создать шлюз для этого протокола на базе Python Gateway и отладить программу на борту с помощью мобильного телефона.
Всё началось с просьбы друга посмотреть его датчик ветра. К сожалению, плата полностью сгнила. Где-то далеко нашлась похожая за 450 долларов, но мы не были уверены, что она поместится в корпус.
Так мы начали знакомиться с протоколом B&G Fastnet. Он основан на последовательном интерфейсе со скоростью 28800 бод. Протокол закрытый, но энтузиастам удалось расшифровать формат сообщений систем H2000 и H3000.
Рисунок 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 лет и заслужили искреннюю привязанность владельца.

