Jeg har i mange år brugt rs485 til at kommunikere med mine IoT-enheder, det har efterhånden udviklet sig til at jeg bruger min egen protokol, over hvilken jeg programmerer og kommunikere med mine dimser.
Men når jeg skriver og tester disse små programmer, er det tit en fordel at gøre det i den virtuelle verden.
RS485 er en halvduplex seriel protokol, der i mit tilfælde kører over en par snoet bus med en hastighed på 100.000 baud og rækker fint over hele min matrikkel. I biler bruger man CAN-bus som ligner meget, hastigheden er her 250.000 baud.
Eftersom rs485 er en broadcast-bus, er det nemt at tilkoble f.ex. en sniffer for at se, hvad der sker på RS-485-nettet.
Jeg bruger i vidudstræḱning USB-rs485-adaptere der fås til ingen penge i Kina (mindre end 10kr), og der er intet i vejen for at bruge flere selv på samme computer f.ex
- MQTT gateway bruger /dev/ttyUSB0
- til at følge trafikken: picocom -l /dev/ttyUSB1 -b 100000
- og det project man arbejder på kunne bruge /dev/ttyUSB2 indtil tingene virkede, og koden kunne flyttes ned på f.ex en Arduino
Efterhånden har det udviklet sig, så jeg tester det meste offline under Linux, hvor tests via scripts og Makefiles øger produktiviteten betragteligt.
Snart løber man tør for USB-stik, og ovenstående scenario kunne jo klares med bare een rs485-USB-adapter, og en virtuel RS485-bus inde i computeren, og det er det denne post drejer sig om.
ttybus
Dette er ikke et nyt project, Copyright noten siger 2010-2021, og source er tilgængelig på:
Example of usage:
Create a new bus called /tmp/ttyS0mux
tty_bus -d -s /tmp/ttyS0mux
Connect a real device to the bus /tmp/ttyS0mux
tty_attach -d -s /tmp/ttyS0mux /dev/ttyS0
Create two fake ttyS0 devices, attached to the bus /tmp/ttyS0mux
tty_fake -d -s /tmp/ttyS0mux /dev/ttyS0.0
tty_fake -d -s /tmp/ttyS0mux /dev/ttyS0.1
Mit lille bidrag er et lille shell-script rs485bus som opretter og forbinder en pseudo-tty til rs485-bussen og returner navnet på denne enhed. kommandoen kan også forbinde fysiske rs485-USB-adaptere til rs485-bussen:
- rs485bus /dev/ttyUSB0
Typisk bruger man $(rs485bus) som i
- picocom -l $(rs485bus)
- mysgw -l $(rs485bus)
- nqload -l $(rs485bus) -U flash:w:firmware.hex:i
- rs485bus -l # viser hvad der er forbundet til rs485-bussen
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME mysgw 1443969 peter 3u CHR 136,3 0t0 6 /dev/pts/3 tstat 1444028 peter 3u CHR 136,7 0t0 10 /dev/pts/7 nqload 1444162 peter 3u CHR 136,9 0t0 12 /dev/pts/9 picocom 1444677 peter 3u CHR 136,12 0t0 15 /dev/pts/12
Ikke raket videnskab, men en lille ting kan reducere mængden af ledninger på spisebordet
#!/bin/bash
# see https://github.com/danielinux/ttybus
#
RS485_MUX=/tmp/rs485mux
RS485_DIR=/tmp/rs485
RS485_DEV=""
if [ "$1" = -l ]; then
lsof $RS485_DIR/* 2> /dev/null
ps -ef | grep \ [t]ty_
exit
fi
mkdir -p $RS485_DIR
if [ ! -S $RS485_MUX ]; then
tty_bus -d -s $RS485_MUX
elif [ -c "$1" ]; then # add real device how about baudrate
picocom -b 100000 --noreset --exit "$1" >/dev/stderr 2>&1
nohup tty_attach -d -s $RS485_MUX $1 &
elif [ $# != 0 ]; then
echo "rs485bus"
echo " return name of a free device"
echo " /dev/tty<*> add this physical tty to rs485bus"
echo " -l show connected programs"
exit
else # return name of free tty on rs485-bus
for i in $RS485_DIR/*; do # re-use unused device
[ $i != $RS485_DIR/\* ] || break
if ! lsof $i; then RS485_DEV=$i; break; fi
done >/dev/null
if [ -z $RS485_DEV ]; then # no unused - create one
n=1
while [ -c $RS485_DIR/$n ]; do n=$((n+1)); done
RS485_DEV=$RS485_DIR/$n
tty_fake -d -s $RS485_MUX $RS485_DEV
sleep 0.1
stty -F "$RS485_DEV" raw -echo
fi
fi
echo $RS485_DEV
NQ485 – Min RS485 protocol
Måske skulle jeg publicere min RS485-kildetekst. Den er ikke hemmelig. NQ485 har nu været stabil i nogle år, og der er ikke umiddelbart noget, jeg vil lave om lige nu. Selve protokollen er beskrevet i source-kodens header file, Så jeg tænkte jeg kunne starte med at vise den:
#ifndef NQ485_H
#define NQ485_H
/*
* nq485.h - Copyright StorePeter Dec 2022 BeerWare ala Phk
*
* Intended to be small to fit in a AVR-bootloader (512/1024)
* and run on small MCU from 8k FLASH and 1k RAM f.ex ATMEGA8
*
* - HOST - typical running Linux inititating trafic on rs485 bus
* - DEVICE - microcontroller embedded, but could be running Linux too
*
* NQ_DG (0x09 ^I) Start-Of-Datagram
* NQ_LF (0x0a ^J LF) End-Of-Datagram
*
* 3 kind of trafic:
* - MySensors-datagram - 1st byte: NQ_DG, 2nd byte: 0, 11-255
* - NQ_XFER-datagram - 1st byte: NQ_DG, 2nd byte: 1-7
* - DebugPrints - 1st byte: NOT NQ_DG
*
* NQ_XFER Datagram from HOST
* NQ_DG <2> ... NQ_LF # Read request
* NQ_DG <3> ... NQ_LF # Write request
* NQ_DG <0,11-255> ... NQ_LF # MySensors transfer
*
* NQ_FER Datagram from DEVICE
* When a station comes to life it will send NRQ_INFO
* NQ_DG <5> = <4|1> ... NQ_LF
* Reply to Master request
* NQ_DG <6> = <4|2> ... NQ_LF # data + ack for read request.
* NQ_DG <7> = <4|3> ... NQ_LF # ack on write request.
* NQ_DG <0,11-255> ... NQ_LF # MySensors transfer
* ... <11-254> NQ_LF # no NQ_DG. DPRINT from node 11-254
*
* Addressing: 11-254, gateway=0 broadcast=255
*
* The NQ_XFER protocol is used to get direct access to ram/flash/eeprom
* on devices for development, debug and flashing, and can operate in
* parallel with MySensors
* - NQ_XFER - 2nd byte: request 1-7 (1s byte in datagram_t)
* - MySenSors - 2nd byte: nodeID 0,11-254,255 (1st byte in datagram_t)
* Special Characters:
* NQ_ESCAPE 0x08 // 0000,1000 escaped: 0x08 \b BS -> 0x08,x48 'H'
* NQ_DG 0x09 // 0000,1001 escaped: 0x09 \t HT -> 0x08,x49 'I'
* NQ_LF 0x0a // 0000,1010 escaped: 0x0a \n LF -> 0x08,x4a 'J'
*
* - values 8,9,10 is escaped with 8 ie. 0x08 -> 0x08 0x48
* - all trafic is to/from Master, NO station-to-station trafic
* - Simple print debuging: "plain text message" <myNodeID> <LF>
*
* Multidrop half-duplex using a single pin, hardware RX, software TX
* for rs485 another pin is needed to control transceiver, could be
* the TX pin and do soft_TX on RX, drawback: code-size and more time
* with disabled irq
*
* address is 8-bit 0x0b-0xfe, 0xff for broadcast, 0 for gateway
*
* The GATEWAY is a Linux box which compared to the embedded systems
* have unlimited resources, from there we can:
* monitor debug prints
* enable/disable them
* reboot a station
* read/write any position in flash/eeprom/ram from any station
* load a new program into a station
* optional: do interactive debugging using Forth
*
* A DEVICE life simple:
* read any byte that shows up
* un-escape escaped characters as they arrive
* put it in buffer.
* add to checksum
* if we receive a command (9 ... 10)
* check first byte to see if it was for us
* if it was for us:
* check sum==0
* copy datagram to in_dg
* handle the request
* <station8> on both send/receive denotes src/dest
* trafic only from/to GATEWAY
* debug printing is really simple:
* wait for no trafic, send it, f.ex.
* +-------------------------------+------+------+
* | "Hello World" from station 17 | 0x11 | 0x0a |
* +-------------------------------+------+------+
* The Gateway could present this to you as
* 17: "Hello World" from station 17
*
* Thats it, happy hacking
* StorePeter
*/
#include
// MySensors Gateway 0 // 0000
// reserved 1 // 0001
#define NRQ_READ 2 // 0010 read request from Master/Gateway
#define NRQ_WRITE 3 // 0011 write request from Master/Gateway
// reserved 4 // 0100
#define NQ_IRQ (4+1) // 0101 unprovoked nq_xfer from device
#define NRPLY_READ (4+2) // 0110 read reply from device
#define NRPLY_WRITE (4+3) // 111 write reply from device
#define NQ_ESCAPE 0x08 // 0000,1000 escaped: 0x08 \b BS -> 0x08,x48 'H'
#define NQ_DG 0x09 // 0000,1001 escaped: 0x09 \t HT -> 0x08,x49 'I'
#define NQ_LF 0x0a // 0000,1010 escaped: 0x0a \n LF -> 0x08,x4a 'J'
#define NQ_MASK 0x0b // 0000,1011
// NQ_IRQ is initially used to send NQT_INFO to HOST on boot-up
// NQ_IRQ INFO has pos 3,4 == node-id, pos4 = [1-8) free for other use
#define NQT_INFO 1 // NRQ_READ
#define NQT_RESET 2 // NRQ_WRITE addr = 0 reset, 0x42 'B' goto bootloader
#define NQT_FLASH 3
#define NQT_RAM 4
#define NQT_EEPROM 5
#define NQT_DPRINT 6 // change dprint level
#define MYS_PAYLOAD_SZ 32 // MyMessage har 5 bit len = 32
#define NQ_PAYLOAD_SZ 32 // nq_xfer has 8 bit len (len to sum)
#define REPLY_HEADER_SZ 3 // req, len, station
#define INBUF_SZ 48 // min 7 + PAYLOAD_SZ
/*
* https://mysensors.org from 2018
* MySensors-2.3.1 is the framework used to transfer sensor data,
* C-version is used on stm8s compiled with SDCC
* Both C++ the MySensor way
* and the C-version is possible on AVR/Linux
*
* 0 1 2 3 msb lsb 4 5 6 7
* +------+--------+------+----------------+---------------------+------+--------+----------
* | last | sender | dest | 5bit 1bit 2bit | 3bit 1bit 1bit 3bit | type | sensor | payload
* +------+--------+------+-|----|----|----+-|----|----|----|----+------+--------+----------
* len flag vers ptyp req ack type
*
* 8 bit - Id of last node this message passed
* 8 bit - Id of sender node (origin)
* 8 bit - Id of destination node
* 5 bit - Length of payload
* 1 bit - Signed flag
* 2 bit - Protocol version
* 3 bit - Payload data type
* 1 bit - Is ack message - Indicator that this is the actual ack message
* 1 bit - Request an ack - Indicator that receiver should send an ack back
* 3 bit - Command type
* 8 bit - Type varies depending on command
* 8 bit - Id of sensor that this message concerns.
*
* NQ_XFER, req =(1,3-7,9} shares dest, type with MySensors
* 0 1 2 3 msb lsb 4 5 6 7
* +------+--------+------+----------------+---------------------+------+--------+----------
* | req | len | dest | addr_l | addr_h | type | addr_e | payload
* +------+--------+------+----------------+---------------------+------+--------+----------
*/
typedef union {
struct { // 1. MySensors Format
uint8_t last;
uint8_t sender;
uint8_t destination;
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
uint8_t protocol:2; uint8_t flag:1; uint8_t data_len:5;
uint8_t cmd_type:3; uint8_t req_ack:1; uint8_t is_ack:1; uint8_t data_type:3;
#else // Default Little Endian (AVR, ESP, ARM, x86)
uint8_t data_len:5; uint8_t flag:1; uint8_t protocol:2;
uint8_t data_type:3; uint8_t is_ack:1; uint8_t req_ack:1; uint8_t cmd_type:3;
#endif
uint8_t type;
uint8_t sensor;
union {
struct { uint8_t bValue; uint8_t bPrecision; } __attribute__((packed));
struct { uint16_t uiValue; uint8_t uiPrecision; } __attribute__((packed));
struct { int16_t iValue; uint8_t iPrecision; } __attribute__((packed));
struct { uint32_t ulValue; uint8_t ulPrecision; } __attribute__((packed));
struct { int32_t lValue; uint8_t lPrecision; } __attribute__((packed));
struct { float fValue; uint8_t fPrecision; } __attribute__((packed));
struct { uint8_t version; uint8_t sensorType; } __attribute__((packed));
char data[MYS_PAYLOAD_SZ];
uint8_t data_u8[MYS_PAYLOAD_SZ];
};
} __attribute__((packed)) mys; // Named for clarity: msg.mys.sender
struct { // 2. NQ_XFER Format (Low-level access)
uint8_t req;
uint8_t len;
uint8_t station;
uint16_t addr;
uint8_t type2;
uint8_t addr_e;
uint8_t xfer_payload[NQ_PAYLOAD_SZ];
} __attribute__((packed)) nq;
struct { // 3. Raw Byte Access
uint8_t header[7];
uint8_t raw_payload[NQ_PAYLOAD_SZ];
} __attribute__((packed)) raw;
} __attribute__((packed)) datagram_t;
#endif // NQ485_H
