Virtual rs485 Network

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
This entry was posted in Arduino, HomeAutomation, rs485. Bookmark the permalink.