From 43952c14ab9a32e76da97700fffe717da49e777e Mon Sep 17 00:00:00 2001 From: Piotr Dobrowolski Date: Fri, 17 Jun 2022 20:01:36 +0200 Subject: [PATCH] software/arduino/ubus: ubus code dump --- protocol.md | 155 ++++++++++++++++ software/arduino/ubus/.gitignore | 2 + software/arduino/ubus/lib/UBus/UBus.cpp | 224 ++++++++++++++++++++++++ software/arduino/ubus/lib/UBus/UBus.h | 50 ++++++ software/arduino/ubus/platformio.ini | 25 +++ software/arduino/ubus/src/main.ino | 42 +++++ software/raspi/ubus.py | 132 ++++++++++++++ 7 files changed, 630 insertions(+) create mode 100644 protocol.md create mode 100644 software/arduino/ubus/.gitignore create mode 100644 software/arduino/ubus/lib/UBus/UBus.cpp create mode 100644 software/arduino/ubus/lib/UBus/UBus.h create mode 100644 software/arduino/ubus/platformio.ini create mode 100644 software/arduino/ubus/src/main.ino create mode 100644 software/raspi/ubus.py diff --git a/protocol.md b/protocol.md new file mode 100644 index 0000000..ea927d5 --- /dev/null +++ b/protocol.md @@ -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 +}; +``` diff --git a/software/arduino/ubus/.gitignore b/software/arduino/ubus/.gitignore new file mode 100644 index 0000000..6c69f4c --- /dev/null +++ b/software/arduino/ubus/.gitignore @@ -0,0 +1,2 @@ +.pioenvs +.piolibdeps diff --git a/software/arduino/ubus/lib/UBus/UBus.cpp b/software/arduino/ubus/lib/UBus/UBus.cpp new file mode 100644 index 0000000..53475f6 --- /dev/null +++ b/software/arduino/ubus/lib/UBus/UBus.cpp @@ -0,0 +1,224 @@ +#include "UBus.h" + +#include +#include + +PN532_HSU pn532hsu(Serial1); +PN532 nfc(pn532hsu); + +#include +#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(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; +} diff --git a/software/arduino/ubus/lib/UBus/UBus.h b/software/arduino/ubus/lib/UBus/UBus.h new file mode 100644 index 0000000..395d0ba --- /dev/null +++ b/software/arduino/ubus/lib/UBus/UBus.h @@ -0,0 +1,50 @@ +#ifndef UBUS_H +#define UBUS_H + +#include +#include + +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 diff --git a/software/arduino/ubus/platformio.ini b/software/arduino/ubus/platformio.ini new file mode 100644 index 0000000..071b299 --- /dev/null +++ b/software/arduino/ubus/platformio.ini @@ -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 diff --git a/software/arduino/ubus/src/main.ino b/software/arduino/ubus/src/main.ino new file mode 100644 index 0000000..299c8f9 --- /dev/null +++ b/software/arduino/ubus/src/main.ino @@ -0,0 +1,42 @@ +#include +#include + +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(); +} diff --git a/software/raspi/ubus.py b/software/raspi/ubus.py new file mode 100644 index 0000000..561324c --- /dev/null +++ b/software/raspi/ubus.py @@ -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('= 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)