software/arduino/ubus: ubus code dump

This commit is contained in:
Piotr Dobrowolski 2022-06-17 20:01:36 +02:00
parent ba3e9a19b4
commit 43952c14ab
7 changed files with 630 additions and 0 deletions

155
protocol.md Normal file
View File

@ -0,0 +1,155 @@
UBus
====
Minimal hdlc-over-half-duplex-rs485 bus protcol.
Bus
---
Bus is running half-duplex 115200bps UART over RS485 line.
Framing
-------
All frames transmitted over a bus are encoded using HDLC rules:
* Start byte (0x7e)
* Message payload
* 0x7d or 0x7e bytes in message payload are substituted with [0x7e, byte ^ 0x20]
* Last 2 bytes of message payload are CRC16 `CRC-CCITT` checksum
* NOTE: byte escaping applies here as well!
* NOTE: checksumming over all message bytes including the checksum should result in checksum 0... (this is a feature of pretty much all CRC algorithms, that, it seems, not many people are aware of)
* Message delimiter (end byte, 0x7e)
* Next message may follow immediately (but won't, see below...)
Addressing
----------
Bus supports 126 addresses
0x00 = bus master
0x01 - 0x3e = bus slave
Most significant bit indicates message direction. Slaves responsing to requests
need to set 0b10000000.
Messages
--------
### Master → slave
```
struct msg {
uint8_t target;
uint8_t request_type;
uint8_t request_payload[]; // depends on message type, may be ommitted
uint16_t checksum; // crc16 ccitt
}
```
### Slave → master
```
struct msg {
uint8_t source; // target ^ 0x80
uint8_t response_type;
uint8_t response_payload[]; // depends on message type, may be ommitted
uint16_t checksum; // crc16 ccitt
}
```
Broadcast messages
------------------
Broadcast messages (target 0xff) are handled TDMA-style. Receiving nodes get
assigned _broadcast_timeslot (20ms) each for their response. Every slave needs
to respond after (addr * _broadcast_timeslot) ms after final request delimiter
byte (0x7e). (TODO: this needs to be properly implemented in serial interrupt
by peeking at last byte received... This is impossible unless we patch standard
Arduino Serial implementation)
20ms is enough for approximately 200 bytes of effective application payload
response for every node.
Message type
------------
### 0x0x System Block
#### 0x00 - Status Poll
Basic bus status poll.
```
struct req { };
struct resp {
uint8_t status;
// 00h - ok
// 01h - ffh = error
};
```
#### 0x01 - Identify
Light up status LED for 0.5s.
```
struct req { };
struct resp { };
```
#### 0x02 - Query Supported Commands
```
struct req { };
struct resp {
uint8_t commands[]; // implemented message IDs
};
```
### 0x1x IO Block
#### 0x10 - GPIO Write
```
struct req {
struct {
uint8_t io_num;
uint8_t value;
} pins[];
};
struct resp {
uint8_t status;
// 00h = ok
// 01h - ffh error;
};
```
#### 0x11 - GPIO Read
```
struct req {
uint8_t pins[];
};
struct resp {
uint8_t values[];
};
```
#### 0x12 - WS2812 Write
```
struct req {
uint8_t pin;
uint8_t data; // packed RGB data, num_leds * 3
};
struct resp {
uint8_t status;
// 00h = ok
// 01h - ffh error;
};
```
### 0x2x Peripheral Block
#### 0x20 - NFC Read
Read NFC reader.
```
struct req { };
struct resp {
uint8_t uid[]; // empty if no card read
};
```

2
software/arduino/ubus/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.pioenvs
.piolibdeps

View File

@ -0,0 +1,224 @@
#include "UBus.h"
#include <PN532_HSU.h>
#include <PN532.h>
PN532_HSU pn532hsu(Serial1);
PN532 nfc(pn532hsu);
#include <FastLED.h>
#define NUM_LEDS 10
#define DATA_PIN 12
CRGB leds[NUM_LEDS];
void UBus::begin() {
RS485Class::begin(115200);
Serial.print("UBus starting up on address ");
Serial.println(addr);
if (addr >= 0x80) {
Serial.println("**** This device is unprovisioned! Disabling responder. ****");
}
// Peripherals
FastLED.addLeds<WS2811, DATA_PIN, RGB>(leds, NUM_LEDS);
FastLED.show();
// PN532 UART needs a pullup
pinMode(11, INPUT_PULLUP);
pinMode(12, INPUT_PULLUP);
nfc.begin();
uint32_t versiondata = nfc.getFirmwareVersion();
// Got ok data, print it out!
Serial.print("Found chip PN5"); Serial.println((versiondata>>24) & 0xFF, HEX);
Serial.print("Firmware ver. "); Serial.print((versiondata>>16) & 0xFF, DEC);
Serial.print('.'); Serial.println((versiondata>>8) & 0xFF, DEC);
nfc.setPassiveActivationRetries(0xFF);
nfc.SAMConfig();
// Blink on bootup
identify_ts = 1;
receive();
}
void UBus::poll() {
if (identify_ts != 0) {
if (identify_ts + 200 > millis()) {
digitalWrite(13, false);
} else {
identify_ts = 0;
digitalWrite(13, true);
}
}
while(available() > 0) parse(read());
uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 }; // Buffer to store the returned UID
uint8_t uidLength; // Length of the UID (4 or 7 bytes depending on ISO14443A card type)
// Wait for an ISO14443A type cards (Mifare, etc.). When one is found
// 'uid' will be populated with the UID, and uidLength will indicate
// if the uid is 4 bytes (Mifare Classic) or 7 bytes (Mifare Ultralight)
bool success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, &uid[0], &uidLength);
if (success) {
for (uint8_t i=0; i < uidLength; i++)
{
Serial.print(uid[i], HEX);
Serial.print(" ");
}
Serial.println("");
}
}
// Encodes a single byte and writes it into am output buffer
void UBus::sendByte(uint8_t b) {
if (b == HDLC_START || b == HDLC_ESC) {
write(HDLC_ESC);
write(b ^ 0x20);
} else {
write(b);
}
}
// Builds and sends a response message
void UBus::send(uint8_t msg_type, uint8_t* buf, size_t size) {
beginTransmission();
uint16_t crc = CRC16_CCITT_INIT_VAL;
write(HDLC_START);
sendByte(addr | 0x80); crc = _crc_ccitt_update(crc, addr | 0x80);
sendByte(msg_type); crc = _crc_ccitt_update(crc, msg_type);
for (size_t i = 0; i < size; i++) {
sendByte(buf[i]); crc = _crc_ccitt_update(crc, buf[i]);
}
sendByte(crc & 0xff);
sendByte(crc >> 8);
write(HDLC_START);
endTransmission();
}
// Handle message residing currently in the buffer
void UBus::handleRequest() {
uint8_t target = buffer[0];
uint8_t msg_type = buffer[1];
uint8_t* payload = buffer + 2;
uint8_t payload_len = buf_pos - 4;
uint8_t resp[16];
// Serial.println(msg_type);
switch (msg_type) {
case SYS_StatusPoll:
resp[0] = 0x00;
sendResponse(msg_type, resp, 1);
break;
case SYS_Identify:
identify_ts = millis();
sendResponse(msg_type, resp, 0);
break;
case SYS_SupportedCommands:
resp[0] = 0x00;
resp[1] = 0x01;
resp[2] = 0x02;
sendResponse(msg_type, resp, 3);
// SYS_StatusPoll, SYS_Identify, SYS_SupportedCommands,
// ...
break;
case GPIO_Write:
for (uint8_t i = 0; i < payload_len / 2; i++) {
pinMode(payload[2 * i], OUTPUT);
digitalWrite(payload[2 * i], payload[2 * i + 1]);
}
resp[0] = 0x00;
sendResponse(msg_type, resp, 1);
break;
case GPIO_Read:
for (uint8_t i = 0; i < payload_len; i++) {
pinMode(payload[i], INPUT_PULLUP);
resp[i] = digitalRead(payload[i]);
}
sendResponse(msg_type, resp, payload_len);
break;
case GPIO_WS2812:
// TODO memcpy?
for (uint8_t i = 0; i < payload_len / 3; i++) {
leds[i].r = payload[3*i];
leds[i].g = payload[3*i + 1];
leds[i].b = payload[3*i + 2];
}
FastLED.show();
sendResponse(msg_type, {}, 0);
break;
}
}
// Sends a response while also handling delayed response in case of broadcast
// messages
void UBus::sendResponse(uint8_t msg_type, uint8_t buf[], size_t size) {
if (buffer[0] == 0xff) {
uint32_t _broadcast_timeslot = 20;
uint32_t offset = 0; // millis() - _hdlc_start;
if (_broadcast_timeslot * addr > offset) {
delay(_broadcast_timeslot * addr - offset); // (millis() - _hdlc_start));
send(msg_type, buf, size);
}
} else {
send(msg_type, buf, size);
}
}
void UBus::parse(uint8_t c) {
if (c == HDLC_ESC) {
escape = true;
return;
}
if (c == HDLC_START) {
escape = false;
// Serial.println(millis() - _hdlc_start);
// 1 byte address, 1 byte msg_type, 2 bytes checksum
if (buf_pos >= 4) {
if (checksum == 0) {
// frame finish - verified
// digitalWrite(13, !digitalRead(13));
if ((buffer[0] == 0xff || buffer[0] == addr) && addr != 0xff) {
handleRequest();
}
} else {
Serial.println("Invalid frame: ");
for (int i = 0; i < buf_pos; i++) { Serial.print(buffer[i], HEX); Serial.print(" "); }
Serial.println(checksum);
// frame invalid
}
}
buf_pos = 0;
checksum = CRC16_CCITT_INIT_VAL;
return;
}
if (buf_pos < HDLC_BUF_SIZE) {
// Serial.println(c);
buffer[buf_pos] = escape ? c ^ 0x20 : c;
checksum = _crc_ccitt_update(checksum, buffer[buf_pos]);
buf_pos += 1;
}
escape = false;
}

View File

@ -0,0 +1,50 @@
#ifndef UBUS_H
#define UBUS_H
#include <ArduinoRS485.h>
#include <util/crc16.h>
extern unsigned long _hdlc_start;
#define HDLC_START 0x7E
#define HDLC_ESC 0x7D
#define HDLC_BUF_SIZE 32
#define CRC16_CCITT_INIT_VAL 0xFFFF
class UBus: public RS485Class {
public:
enum MessageType : uint8_t {
SYS_StatusPoll = 0x00,
SYS_Identify = 0x01,
SYS_SupportedCommands = 0x02,
GPIO_Write = 0x10,
GPIO_Read = 0x11,
GPIO_WS2812 = 0x12,
PERI_NFC = 0x20,
};
uint8_t addr = 0;
uint32_t identify_ts = 0;
bool escape = false;
uint8_t buf_pos = 0;
uint8_t buffer[HDLC_BUF_SIZE];
uint16_t checksum = CRC16_CCITT_INIT_VAL;
UBus(): RS485Class(Serial, 1, 3, 2) { }
void begin();
void sendByte(uint8_t b);
void send(uint8_t msg_type, uint8_t* buf, size_t size);
void sendResponse(uint8_t msg_type, uint8_t* buf, size_t size);
void handleRequest();
void poll();
void parse(uint8_t c);
};
#endif

View File

@ -0,0 +1,25 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:uno]
platform = atmelavr
board = atmega328pb
build_flags = -D SERIAL_PORT_HARDWARE=Serial
framework = arduino
upload_flags = -F
monitor_port = /dev/serial/by-id/usb-FTDI_TTL232R_FTF48YOZ-if00-port0
upload_port = /dev/serial/by-id/usb-FTDI_TTL232R_FTF48YOZ-if00-port0
targets = upload, monitor
monitor_speed = 115200
lib_deps =
Wire
arduino-libraries/ArduinoRS485@^1.0.0
fastled/FastLED@^3.4.0
https://github.com/yoshitake-hamano/PN532

View File

@ -0,0 +1,42 @@
#include <EEPROM.h>
#include <UBus.h>
const int RS485_RE = 2;
const int RS485_DE = 3;
UBus bus;
void provision() {
long start_ts = millis();
Serial.begin(115200);
Serial.println("This device is unprovisioned!");
while (start_ts + 10000 > millis()) {
Serial.print("Address: ");
long addr = Serial.parseInt(SKIP_WHITESPACE);
if (addr < 0x01 || addr > 0x80) {
Serial.println("Invalid address...");
continue;
}
EEPROM.write(0x00, addr);
bus.addr = addr;
Serial.println("Finished.");
return;
}
}
void setup() {
pinMode(13, OUTPUT);
bus.addr = EEPROM.read(0);
if (bus.addr == 0xff) {
provision();
}
bus.begin();
}
void loop() {
bus.poll();
}

132
software/raspi/ubus.py Normal file
View File

@ -0,0 +1,132 @@
from serial import Serial
# from PyCRC.CRCCCITT import CRCCCITT
import time
import struct
def lo8(b): return b & 0xff
def hi8(b): return (b >> 8) & 0xff
def crc16(buf):
'''
uint16_t
crc_ccitt_update (uint16_t crc, uint8_t data)
{
data ^= lo8 (crc);
data ^= data << 4;
return ((((uint16_t)data << 8) | hi8 (crc)) ^ (uint8_t)(data >> 4)
^ ((uint16_t)data << 3));
}
'''
crc = 0xffff
for data in buf:
data ^= (crc) & 0xff
data ^= (data << 4) & 0xff
crc = (((data << 8) | (crc >> 8) & 0xff) ^ (data >> 4) ^ (data << 3)) & 0xffff
return crc
HDLC_START = b'\x7e'
HDLC_ESC = b'\x7d'
class UBus(Serial):
def _send(self, bar):
self.write(HDLC_START)
checksum = crc16(bar)
# CRCCCITT("FFFF").calculate(bar)
for b in bar + struct.pack('<H', checksum):
if bytes([b]) in [HDLC_START, HDLC_ESC]:
self.write(HDLC_ESC)
self.write(bytes([b ^ 0x20]))
else:
self.write(bytes([b]))
self.write(HDLC_START)
self.flush()
def request(self, target, msg_type, payload=bytearray(), retry=0):
for n in range(retry + 1):
try:
self._send(bytearray([target, msg_type]) + bytearray(payload))
return self.read_message()[2:]
except TimeoutError as exc:
if n == retry:
raise exc
def request_broadcast(self, msg_type, payload=bytearray()):
self._send(bytearray([0xff, msg_type]) + bytearray(payload))
responses = []
while True:
try:
yield self.read_message()
except TimeoutError:
return
buf = bytearray()
escape = False
def read_message(self):
res = None
while res is None:
res = self._read()
return res
def _read(self):
b = self.read(1)
if not b:
raise TimeoutError
if b == HDLC_ESC:
self.escape = True
return None
if b == HDLC_START:
res = None
if len(self.buf) >= 2:
if crc16(self.buf) == 0:
res = self.buf[:-2]
else:
print('INVALID CRC:', self.buf, crc16(self.buf))
self.escape = False
self.buf = bytearray()
return res
self.buf.append(ord(b) ^ 0x20 if self.escape else ord(b))
self.escape = False
if __name__ == '__main__':
# bus = UBus('/dev/serial/by-id/usb-1a86_USB2.0-Serial-if00-port0', 115200)
bus = UBus('/dev/serial/by-id/usb-1a86_USB2.0-Ser_-if00-port0', 115200, timeout=0.1)
# bus = UBus('/dev/serial/by-id/usb-FTDI_TTL232R_FTF48YOZ-if00-port0', 115200)
while True:
#try:
# print(bus.request(0x01,0x01))
#except Exception as exc:
# print(repr(exc))
#time.sleep(1.0)
#for m in list(bus.request_broadcast(0x00)):
# print(m)
#print(list(bus.request_broadcast(0x00))) # , bytearray([13]))))
#print(list(bus.request_broadcast(0x11, bytearray([13]))))
try:
#print(list(bus.request_broadcast(0x00))) # , bytearray([13]))))
resp = bus.request(0x04, 0x11, [9, 10,11,12])
print(resp)
colors = sum([[255, 0, 0] if b else [0, 0, 0] for b in resp], [])
print(colors)
bus.request(0x02, 0x12, colors) # [32] * 24, 3) # [0,0,0,0,0,0,0,0,0,255,0,255,255,255,0,0,255,255])
except TimeoutError as exc:
print(repr(exc))
#print('bep')
#bus._send(bytearray([0xff, 0x00]))
#time.sleep(0.1)
#while bus.inWaiting():
# frame = bus._read()
# if frame:
# print(frame)