-
Notifications
You must be signed in to change notification settings - Fork 0
/
protocol.txt
177 lines (137 loc) · 12.9 KB
/
protocol.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
Domotic protocol (version 0.1.1)
================================
When using IP as transport layer, every node must have its own address and must be able to receive and send data *only* from/to the local network.
This protocol is *not* intended to be exposed to the Internet, even if some (limited) coutermeasures are implemented: multicast TTL is 1, longer answers require authenticated requests, etc. If accessibility from the Internet is needed, a gateway with proper security measures (SSL encryption, proper authentication, etc) *must* be used. The gateway can translate to other protocols, too.
The packet format is constrained by UDP on ESP8266 (reference platform) that allows at most 1472 bytes per UDP packet (a single Ethernet frame).
Default UDP port used is 55114. Default multicast address used is 239.255.215.74 (see RFC2365 "The IPv4 Local Scope", the last two octects are the integer 55114).
If needed, "fake" MAC addresses can be generated as 4e:64:4b:xx:yy:zz ('N':'d':'K':xx:yy:zz) -- least significant bits of first octect are '10', that means "locally administerd (1)" and "unicast (0)".
Every node in the network is configured with an unique ID ('sender'). 0x0000 means "unconfigured" and 0xFFFF is reserved.
Every IO line MUST be member of a groupID (see maps in registers 0x20-0x23), that is used to report its changes. ID 0000 is reserved for unconfigured IOs that MUST NOT be reported. GroupID allocation strategy is instance-specific and not covered by protocol.
Allowed characters for strings in array elements are [a-zA-Z0-9.:_-] .
Data coming throught the serial port during the initial configuration (after a hard config reset -- usually a 10-seconds press of a protected button) is considered signed and encrypted (but it's actually unsigned plaintext).
Display-enabled devices MIGHT handle "widgets" (bars, icons) as output ports (both analog and digital). If message is longer than allowed the device MUST show at least the first max_msg_len characters and MIGHT use scrolling to display the whole message.
A packet is defined as:
Pkt := UnicastPkt | MulticastPkt
UnicastPkt := RequestPkt | AnswerPkt
RequestPkt is sent from a client to a server to initiate an operation.
RequestPkt := SimplePkt | SignedPkt | EncPkt
SimplePkt := CommandPacket | InfoPacket
CommandPacket := <'C'> {DigitalOutSpec | AnalogOutSpec | RegisterSpec}
DigitalOutSpec := <'D'> <output#:ByteHex> <'0'|'1'|'T'>
AnalogOutSpec := <'A'> <input#:ByteHex> <value:WordHex>
WordHex := <hibyte:ByteHex> <lobyte:ByteHex>
RegisterSpec := <'R'> <reg#:ByteHex> <0x20-0x7f>* // Till end of line or end of packet; register-specific parsing required
InfoPacket := <'I'> {DigitalReadSpec | AnalogReadSpec | InfoSpec | RegisterReadSpec}
DigitalReadSpec := <'D'> <'I'|'O'> <io#:ByteHex>
AnalogReadSpec := <'A'> <'I'|'O'> <io#:ByteHex>
InfoSpec := <'I'> <'A'|'D'> <'I'|'O'> <io#:ByteHex>
RegisterReadSpec := <'R'> <reg#:ByteHex> [<arrayelement#:ByteHex>]
SignedPkt := <'S'> <keyID:WordHex> <signature:Array<ByteHex>> SimplePkt // TODO
EncPkt := <'E'> <keyID:WordHex> <payload:Array<ByteHex>> // Payload can be either SignedPkt or SimplePkt, and MUST be encrypted under given key; TODO
ByteHex := <0-9|A-F><0-9|A-F> // Max 256 'lines' per type
AnswerPkt is sent from a server to the client that sent a RequestPkt. If it's sent, it MUST be addressed to the same IP and port that originated the RequestPkt.
In SignedPkt (and the following SignedUpdatePkt) the signature covers only payload data (SimplePkt or EventSpec), *NOT* including termination character (CR or \x00).
TODO: handle encryption (should use the same key as in request, if possible: what happens when using pubkeys?)
AnswerPkt := <'A'> ErrorCode AnswerSpec
ErrorCode := <err:ByteHex>
AnswerSpec := <''> | <current status string depending on received SimplePkt>
Nonzero 'err' reports an error and AnswerSpec must be empty.
If err is zero, AnswerSpec depends on SimplePkt:
CD: <'W'> <'D'> <'0'|'1'>
CA: <'W'> <'A'>
CR: <'W'> <'R'>
ID: <'R'> <'D'> <'0'|'1'>
IA: <'R'> <'A'> <value#:WordHex>
IR: <'R'> <'R'> {<'L'> <len:ByteHex> | <'V'> <0x20-0x7f>*}
II: <'R'> <'I'> {InfoBool | InfoPercent | InfoTemp | InfoPower | InfoUserFloat | InfoText} <descr:<0x20-0x7f>*>
InfoBool := <'B'> // Used for digital lines
InfoPercent := <'%'> <decimals:0-3>
InfoTemp := <'K'> <decimals:0-2> // By default room temperatures are reported in centi-Kelvin (K2)
InfoPower := <'W'> <decimals:0-2> // Power in Watts (0.00 to 65535) -- usually home appliances use decimals=0 (1W resolution)
InfoUserFloat := <'F'> <numerator:WordHex> <denominator:WordHex> // Float=(numerator*regvalue)/(denominator*65536); note that result is strictly < numerator/denominator
InfoText := <'T'> <maxlen:ByteHex>
MulticastPkt can be generated either in response to some external event (a change in temperature, a timer expiring, ...) or in response to a RequestPkt.
Nodes can listen to multicast packets to update their state or act in response to events happened on other nodes (f.e. a light turns on when a button on another node is pressed).
Note that setting the node HW config (which lines are inputs, which are outputs, their names and characteristics, etc) is outside the scope of this protocol, and nodes should not allow remote changes.
MulticastPkt:= UpdatePkt | SignedUpdatePkt
SignedUpdatePkt := 'S' <keyID:WordHex> <signature:urlsafe_base64_string> UpdatePkt
UpdatePkt := <'U'> EventSpec
EventSpec := InEvent | OutEvent | TimeEvent
InEvent := <'I'> {DigitalEvent | AnalogEvent}
DigitalEvent := <'D'> <group:WordHex> <'0'|'1'>
AnalogEvent := <'A'> <group:WordHex> <value:WordHex>
OutEvent := <'O'> {DigitalEvent | AnalogEvent}
TimeEvent := <'T'> <epoch:ByteHex> <hictr:WordHex> <loctr:WordHex> <tz_dst:ByteHex>
Analog values are represented as an unsigned 16-bits word. Temperatures should be converted in centi-Kelvin unless peculiar needs require otherwise.
TimeEvent is usually generated periodically (at least every minute, up to once per second) by a coordinator node with access to accurate timekeeping HW and/or external sources (NTP, GPS, etc). The only epoch currently defined is 00 (for Unix epoch: seconds since Jan, 1, 1970 -- will last till Jan 2106), hictr and loctr are a single 32-bit counter. tz_dst packs timezone relative to GMT in lower 5 bits (signed) and if dst is active or not in bit 5. Bits 6 and 7 are reserved and must be 0. Security-sensitive nodes must sanitize it (checking that it does not deviate too much from internal clock, adding sequence numbers to packets sent during the same second, etc) and should only trust signed updates. Time needed for signature verification *must not* "shift" the timestamp (in other words the timestamp references the second the packet have been received, not the one it gets processed).
Only "meaningful" AnalogEvent should be reported. The meaningfulness of the change is application- and sensor-specific. For example, it's usually useless to report every .01°C change in temperature for a room, but only a .1°C change -- or even a 2°C one if you're using a DHT11 (2°C error!). Rule of thumb: don't send more than a report every 10 seconds or so for a single entity to avoid flooding the network with useless data.
Registers
=========
When reading register containing arrays, if the optional argument is omitted, only the array len is returned. If the optional argument is given, then the actual element is returned.
Every write to registers must be inside a signed packet.
Changes to register contents must be saved to permanent storage before returning answer packet.
00: node info
READ: string: "protocol_version digital_outs analog_outs digital_ins analog_ins max_txt_len utf8_support"; protocol_version is currently "0.1.1", numbers in *decimal*, utf8_support as 'T' or 'F'
WRITE: (requires a signed packet *and* 'sender' still 0x0000) sets 'sender' (node-ID)
01: node keys & supported algorithms
READ: Array<String>: "<keyID:WordHex>:algo=key_handle;uses", algo:='ec25519'(key exchange)|'ed25519'(sign)|'nistp256'(HW-backed, f.e. in ATECC608A)|'aes-enc'|'aes-mac'|'unset'; empty array means neither authentication nor encryption are supported, else number of slots tells the number of keys that can be used; key_handle for public and private keys is the public key (you can say if the priv key is available by looking at key usage), for secret keys is just a key handle (FIXME: define how it's generated), if the key is not yet set but the slot is reserved for a certain algo, handle can be empty; 'uses' is a string of one or more permitted uses for the given key: 'e'ncrypt, 'k'ey agreement, 's'ignature, 'v'erify signature (only pubkey is present); keys can only be set using encrypted packets or OOB; keyID 0000 is reserved for the NULL method (signature generates a sequence of 128 '0', encryption copies the source string) and *MUST* be refused in production builds; keyID FFFF is reserved and can not be used
02: hostname (short)
READ: string: "<hostname> (<build_ID>)"
WRITE: string, max 64 characters ([a-z0-9-], see RFC1123)
03: flags; WordHex, TODO (UdpReportEnable?) -- currently RESERVED
04: network info
READ: string: "WIFI:SSID,ip" | "ETH:ip" | "BUS:nodeID"
WRITE: string: "WIFI:SSID,password[,ip,mask]" (must also be encrypted) | "ETH:DHCP|ip,mask" | "BUS:nodeID" (bus must be on a different port than config interface)
05: display message
READ: string
WRITE: string, max max_txt_len (from R0) printable-ASCII characters (0x20-0x7e) for message
06-1f: reserved
20: digital out port map
READ: Array<WordHex> : current mapping of digital outs
WRITE: Array<WordHex> : sets new mapping of digital outs
21: analog out port map
READ: Array<WordHex> : current mapping of analog outs
WRITE: Array<WordHex> : sets new mapping of analog outs
22: digital in port map
READ: Array<WordHex> : current mapping of digital ins
WRITE: Array<WordHex> : sets new mapping of digital ins
23: analog in port map
READ: Array<WordHex> : current mapping of analog ins
WRITE: Array<WordHex> : sets new mapping of analog ins
24: (Array<Event>) events
25: (Array<?>) timer actions TODO
26: (Array<WordHex>) timers intervals; most significant nibble is: 0=Monostable|1=Astable, 00=milliseconds, 01=seconds, 10=minutes, 11=hours, 0=RESERVED; the remaining 12 bits define the actual interval
27-ff: reserved
/*************************************
TO BE RETHOUGHT COMPLETELY !
||||||||||||||||||||||||||||
vvvvvvvvvvvvvvvvvvvvvvvvvvvv
Events and timers
=================
An "event" is the reaction of a node to a *multicast* message, irregardless if it's being transmitted or received. Unicast messages don't trigger event processing.
A timer is a node-private object that (obviously) keeps track of time. It can only be started (starts from the definition in R27) and gets decremented automatically every tick (.1s = 10Hz). When it reaches 0, it fires an event. Timers need not to be very accurate (+/- a tick is still OK). Setting a timer to 0 disables it *without* firing an event. Only a single action is allowed for a timer.
Events are thought for simple actions ('turn off a light after 30 seconds'). Complex scenarios COULD be coordinated by a management node (that COULD offer a more user-friendly interface or even a programmatic one). Anyway "semi-complex" scenarios are possible even with only a limited amount of events.
Event := GroupID Condition {DigitalOutSpec | AnalogOutSpec | TimerSpec}
Condition := <'-'> | EventCond | Timer
EventCond := EventSpec [<'<'> | <'>'> | <'='>] // Default is '='
Timer := <'T'> ByteHex
TimerSpec := Timer {<'0'> | <'1'>]
TimerEvent:= Timer DigitalOutSpec
The first match is on groupID. The second match is on Condition. '-' means the action is executed without checking anything.
Examples (spaces added for readability):
// Stepper: toggles out1 state every time digital input 4 of group 1234 goes low
1234 ID04 0 D01 T
// Control heating system with hysteresis
ABCD IA01 21C0 > D03 0 // Turn off heating if temp > 21.0°C
ABCD IA01 17C0 < D03 1 // Turn on heating if temp < 17.0°C
// Toggle out2 when in7 of node 4567 goes low for more than 1s (R28 must contain "..,..,0A,...")
4567 ID07 1 T02 0 // 4567:din7 is high: disable timer2
4567 ID07 0 T02 1 // start timer2 (multicast is sent only when the signal is changing)
T02 D02 T // timer2 fires: toggle out2
// --Not working-- Watchdog: other nodes are powered only when out1=1
// Note that T00 is only started once a packet arrives, so even if the AP takes 10minutes to come back online it's not a problem!
* - T00 0384 // restart timer 00 every time a multicast packet arrives; fires after 90s (900 = 0x0384)
! T00 D01 0 // when timer0 fires, out1 gets reset
! T00 T01 000A // ... and timer1 is started (1s)
! T01 D01 1 // When timer1 expires, out1 returns to 1
*/