src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
32 / 32
100.0%
Branches:
10 / 10
100.0%
Functions:
13 / 13
100.0%

ProtoSchema.h
Line Branch Exec Source
1 #ifndef PROTO_SCHEMA_H
2 #define PROTO_SCHEMA_H
3 #include <stdint.h>
4 #include <type_traits>
5 #include "JsonFormatter.h"
6
7 // Socket protocol 2 (docs/PROTOCOL.md, docs/adr/0005-one-websocket-protocol.md):
8 // the one table of message types, fields, error codes and feeds. The device
9 // sends it to clients as the schema, so it is the only copy anywhere. Writers
10 // name fields by proto_field_t, so a field that isn't declared here can't be
11 // sent. Kept free of the shade model and the web server so it can be unit
12 // tested on the PC (pio test -e native).
13
14 #define PROTO_VERSION 2
15
16 // Message types: 1-15 control, 16-127 client ops, 128-255 device events. A
17 // number is never reused; a retired type stays reserved.
18 // TYPE(name, number, sender, op): sender is 'c' client, 'd' device, 'b' both;
19 // op is what becomes of a client op (proto_op_t), '-' for the rest.
20 #define PROTO_TYPES(TYPE) \
21 TYPE(hello, 1, 'd', '-') \
22 TYPE(reply, 2, 'd', '-') \
23 TYPE(next, 3, 'c', '-') \
24 TYPE(sync, 4, 'b', '-') \
25 TYPE(syncEnd, 5, 'd', '-') \
26 TYPE(subscribe, 6, 'c', '-') \
27 TYPE(unsubscribe, 7, 'c', '-') \
28 TYPE(shadeCommand, 16, 'c', 'q') \
29 TYPE(groupCommand, 17, 'c', 'q') \
30 TYPE(tiltCommand, 18, 'c', 'q') \
31 TYPE(repeatCommand, 19, 'c', 'q') \
32 TYPE(setPositions, 20, 'c', 'q') \
33 TYPE(setSensor, 21, 'c', 'q') \
34 TYPE(setMyPosition, 22, 'c', 'q') \
35 TYPE(unpairShade, 23, 'c', 'q') \
36 TYPE(setRollingCode, 24, 'c', 'q') \
37 TYPE(sendRemoteCommand, 25, 'c', 'f') \
38 TYPE(addRoom, 26, 'c', 'e') \
39 TYPE(addShade, 27, 'c', 'e') \
40 TYPE(addGroup, 28, 'c', 'e') \
41 TYPE(saveRoom, 29, 'c', 'e') \
42 TYPE(saveShade, 30, 'c', 'e') \
43 TYPE(saveGroup, 31, 'c', 'e') \
44 TYPE(deleteRoom, 32, 'c', 'e') \
45 TYPE(deleteShade, 33, 'c', 'e') \
46 TYPE(deleteGroup, 34, 'c', 'e') \
47 TYPE(linkRemote, 35, 'c', 'e') \
48 TYPE(unlinkRemote, 36, 'c', 'e') \
49 TYPE(linkToGroup, 37, 'c', 'e') \
50 TYPE(unlinkFromGroup, 38, 'c', 'e') \
51 TYPE(linkRepeater, 39, 'c', 'e') \
52 TYPE(unlinkRepeater, 40, 'c', 'e') \
53 TYPE(setPaired, 41, 'c', 'e') \
54 TYPE(roomSortOrder, 42, 'c', 'e') \
55 TYPE(shadeSortOrder, 43, 'c', 'e') \
56 TYPE(groupSortOrder, 44, 'c', 'e') \
57 TYPE(getNextRoom, 45, 'c', 'r') \
58 TYPE(getNextShade, 46, 'c', 'r') \
59 TYPE(getNextGroup, 47, 'c', 'r') \
60 TYPE(groupOptions, 48, 'c', 'r') \
61 TYPE(getSettings, 49, 'c', 'd') \
62 TYPE(saveSettings, 50, 'c', 'd') \
63 TYPE(scanWifi, 51, 'c', 'd') \
64 TYPE(beginFrequencyScan, 52, 'c', 'd') \
65 TYPE(endFrequencyScan, 53, 'c', 'd') \
66 TYPE(reboot, 54, 'c', 'd') \
67 TYPE(shadeState, 128, 'd', '-') \
68 TYPE(shade, 129, 'd', '-') \
69 TYPE(shadeRemoved, 130, 'd', '-') \
70 TYPE(group, 131, 'd', '-') \
71 TYPE(groupState, 132, 'd', '-') \
72 TYPE(groupRemoved, 133, 'd', '-') \
73 TYPE(room, 134, 'd', '-') \
74 TYPE(roomRemoved, 135, 'd', '-') \
75 TYPE(roomOrder, 136, 'd', '-') \
76 TYPE(shadeOrder, 137, 'd', '-') \
77 TYPE(groupOrder, 138, 'd', '-') \
78 TYPE(shadeCommanded, 139, 'd', '-') \
79 TYPE(memory, 140, 'd', '-') \
80 TYPE(wifi, 141, 'd', '-') \
81 TYPE(ethernet, 142, 'd', '-') \
82 TYPE(frame, 143, 'd', '-') \
83 TYPE(frequencyScan, 144, 'd', '-') \
84 TYPE(repeaters, 145, 'd', '-') \
85 TYPE(settingsChanged, 146, 'd', '-') \
86 TYPE(wifiNetworks, 147, 'd', '-')
87
88 // Fields, in one dictionary for every type. FIELD(name, key, kind, default):
89 // kind is b bool (sent as 1/0), i integer, n number with a fraction, s string,
90 // l list, o object. A field at its default is left out; a string's default is
91 // "" and a list's is empty. hello's and the schema's own keys (fw, sh, T, F,
92 // E, Q) are fixed, as a client needs them before it has the schema.
93 #define PROTO_FIELDS(FIELD) \
94 /* the schema and the sync */ \
95 FIELD(types, "T", 'l', 0) \
96 FIELD(fields, "F", 'l', 0) \
97 FIELD(errors, "E", 'l', 0) \
98 FIELD(feeds, "Q", 'l', 0) \
99 FIELD(header, "h", 'o', 0) \
100 FIELD(rooms, "R", 'l', 0) \
101 FIELD(shades, "S", 'l', 0) \
102 FIELD(groups, "G", 'l', 0) \
103 FIELD(repeaters, "X", 'l', 0) \
104 FIELD(status, "Z", 'o', 0) \
105 FIELD(fw, "fw", 's', 0) \
106 FIELD(schemaHash, "sh", 's', 0) \
107 /* replies and control */ \
108 FIELD(ok, "k", 'b', 0) \
109 FIELD(code, "e", 'i', 0) \
110 FIELD(desc, "x", 's', 0) \
111 FIELD(feed, "fe", 'i', 0) \
112 FIELD(ids, "is", 'l', 0) \
113 /* rooms, shades and groups */ \
114 FIELD(shadeId, "s", 'i', 0) \
115 FIELD(groupId, "g", 'i', 0) \
116 FIELD(roomId, "r", 'i', 0) \
117 FIELD(name, "n", 's', 0) \
118 FIELD(remoteAddress, "a", 'i', 0) \
119 FIELD(lastRollingCode, "c", 'i', 0) \
120 FIELD(sortOrder, "o", 'i', 0) \
121 FIELD(flags, "f", 'i', 0) \
122 FIELD(position, "p", 'i', 0) \
123 FIELD(target, "t", 'i', 0) \
124 FIELD(direction, "d", 'i', 0) \
125 FIELD(myPos, "m", 'i', -1) \
126 FIELD(tiltType, "y", 'i', 0) \
127 FIELD(tiltPosition, "q", 'i', 0) \
128 FIELD(tiltTarget, "u", 'i', 0) \
129 FIELD(tiltDirection, "w", 'i', 0) \
130 FIELD(myTiltPos, "M", 'i', -1) \
131 FIELD(upTime, "U", 'i', 0) \
132 FIELD(downTime, "D", 'i', 0) \
133 FIELD(tiltTime, "Y", 'i', 0) \
134 FIELD(stepSize, "z", 'i', 100) \
135 FIELD(paired, "i", 'b', 0) \
136 FIELD(shadeType, "st", 'i', 0) \
137 FIELD(bitLength, "bl", 'i', 56) \
138 FIELD(proto, "pr", 'i', 0) \
139 FIELD(flipCommands, "fc", 'b', 0) \
140 FIELD(flipPosition, "fp", 'b', 0) \
141 FIELD(inGroup, "ig", 'b', 0) \
142 FIELD(sunSensor, "ss", 'b', 0) \
143 FIELD(light, "li", 'b', 0) \
144 FIELD(simMy, "sm", 'b', 0) \
145 FIELD(repeats, "rp", 'i', 1) \
146 FIELD(gpioUp, "gu", 'i', 0) \
147 FIELD(gpioDown, "gd", 'i', 0) \
148 FIELD(gpioMy, "gm", 'i', 0) \
149 FIELD(gpioLLTrigger, "gl", 'b', 0) \
150 FIELD(linkedRemotes, "lk", 'l', 0) \
151 FIELD(linkedShades, "ls", 'l', 0) \
152 /* the command ops and shadeCommanded */ \
153 FIELD(command, "cm", 's', 0) \
154 FIELD(source, "so", 's', 0) \
155 FIELD(sourceAddress, "sr", 'i', 0) \
156 FIELD(repeat, "re", 'i', 0) \
157 FIELD(sunny, "sn", 'b', 0) \
158 FIELD(windy, "wn", 'b', 0) \
159 /* the edit ops */ \
160 FIELD(validation, "vc", 'i', 0) \
161 /* the header */ \
162 FIELD(maxRooms, "xr", 'i', 0) \
163 FIELD(maxShades, "xs", 'i', 0) \
164 FIELD(maxGroups, "xg", 'i', 0) \
165 FIELD(maxGroupedShades, "xm", 'i', 0) \
166 FIELD(maxLinkedRemotes, "xl", 'i', 0) \
167 FIELD(startingAddress, "sa", 'i', 0) \
168 FIELD(serverId, "sv", 's', 0) \
169 FIELD(hostname, "hn", 's', 0) \
170 FIELD(model, "md", 's', 0) \
171 FIELD(chipModel, "cp", 's', 0) \
172 FIELD(hasOtaSlot, "os", 'b', 1) \
173 FIELD(connType, "ct", 's', 0) \
174 /* memory, wifi and ethernet */ \
175 FIELD(freeHeap, "fr", 'i', 0) \
176 FIELD(maxHeap, "mx", 'i', 0) \
177 FIELD(minHeap, "mn", 'i', 0) \
178 FIELD(totalHeap, "to", 'i', 0) \
179 FIELD(uptime, "up", 'i', 0) \
180 FIELD(loopRate, "lp", 'i', 0) \
181 FIELD(loopMaxUs, "lx", 'i', 0) \
182 FIELD(ssid, "sd", 's', 0) \
183 FIELD(strength, "sg", 'i', 0) \
184 FIELD(channel, "ch", 'i', 0) \
185 FIELD(connected, "co", 'b', 0) \
186 FIELD(speed, "sp", 'i', 0) \
187 FIELD(fullDuplex, "fd", 'b', 0) \
188 /* the frame and frequency scan feeds */ \
189 FIELD(encKey, "ek", 'i', 0) \
190 FIELD(rawCmd, "rw", 'i', 0) \
191 FIELD(rssi, "rs", 'i', 0) \
192 FIELD(bits, "bt", 'i', 0) \
193 FIELD(valid, "va", 'b', 0) \
194 FIELD(hwsync, "hs", 'i', 0) \
195 FIELD(pulses, "pu", 'l', 0) \
196 FIELD(scanning, "sc", 'b', 0) \
197 FIELD(testFreq, "tf", 'n', 0) \
198 FIELD(testRssi, "tr", 'i', 0) \
199 FIELD(frequency, "fq", 'n', 0) \
200 /* the settings ops: which section, and what a save did */ \
201 FIELD(section, "se", 's', 0) \
202 FIELD(apiKey, "ak", 's', 0) \
203 FIELD(rebooting, "rb", 'b', 0) \
204 /* general */ \
205 FIELD(ssdpBroadcast, "sb", 'b', 0) \
206 FIELD(ntpServer, "ns", 's', 0) \
207 FIELD(posixZone, "pz", 's', 0) \
208 /* network: the connection the settings ask for (connType is the one in \
209 use), WiFi, Ethernet and the IP address */ \
210 FIELD(connMode, "cn", 'i', 0) \
211 FIELD(hasEthernet, "he", 'b', 0) \
212 FIELD(passphrase, "pp", 's', 0) \
213 FIELD(hasPassphrase, "hq", 'b', 0) \
214 FIELD(roaming, "ro", 'b', 0) \
215 FIELD(hidden, "hd", 'b', 0) \
216 FIELD(boardType, "bo", 'i', 0) \
217 FIELD(phyType, "ph", 'i', 0) \
218 FIELD(CLKMode, "ck", 'i', 0) \
219 FIELD(phyAddress, "pa", 'i', 0) \
220 FIELD(PWRPin, "pw", 'i', 0) \
221 FIELD(MDCPin, "mc", 'i', 0) \
222 FIELD(MDIOPin, "mi", 'i', 0) \
223 FIELD(dhcp, "dh", 'b', 0) \
224 FIELD(ip, "ip", 's', 0) \
225 FIELD(gateway, "gw", 's', 0) \
226 FIELD(subnet, "su", 's', 0) \
227 FIELD(dns1, "d1", 's', 0) \
228 FIELD(dns2, "d2", 's', 0) \
229 /* MQTT and security; hostname is the broker's in MQTT */ \
230 FIELD(enabled, "en", 'b', 0) \
231 FIELD(pubDisco, "pd", 'b', 0) \
232 FIELD(protocol, "pt", 's', 0) \
233 FIELD(port, "po", 'i', 0) \
234 FIELD(username, "un", 's', 0) \
235 FIELD(password, "ps", 's', 0) \
236 FIELD(hasPassword, "hw", 'b', 0) \
237 FIELD(rootTopic, "rt", 's', 0) \
238 FIELD(discoTopic, "dt", 's', 0) \
239 FIELD(securityType, "sy", 'i', 0) \
240 FIELD(pin, "pn", 's', 0) \
241 FIELD(hasPin, "hi", 'b', 0) \
242 /* the radio: bitLength, proto, frequency and enabled as above */ \
243 FIELD(TXPin, "tx", 'i', 0) \
244 FIELD(RXPin, "rx", 'i', 0) \
245 FIELD(SCKPin, "sk", 'i', 0) \
246 FIELD(MOSIPin, "mo", 'i', 0) \
247 FIELD(MISOPin, "ms", 'i', 0) \
248 FIELD(CSNPin, "cs", 'i', 0) \
249 FIELD(rxBandwidth, "bw", 'n', 0) \
250 FIELD(deviation, "dv", 'n', 0) \
251 FIELD(txPower, "tp", 'i', 0) \
252 FIELD(noiseDetection, "nd", 'b', 0) \
253 FIELD(radioInit, "ri", 'b', 0) \
254 /* wifiNetworks: each network has ssid, channel and strength as above */ \
255 FIELD(networks, "nw", 'l', 0) \
256 FIELD(macAddress, "ma", 's', 0)
257
258 // Error codes in a reply. ERR(name, number)
259 #define PROTO_ERRORS(ERR) \
260 ERR(badRequest, 1) \
261 ERR(unknownOp, 2) \
262 ERR(unauthorized, 3) \
263 ERR(notFound, 4) \
264 ERR(busy, 5) \
265 ERR(radioUnavailable, 6) \
266 ERR(internal, 7)
267
268 // Feeds a client can subscribe to. FEED(name, number)
269 #define PROTO_FEEDS(FEED) \
270 FEED(frames, 1) \
271 FEED(frequencyScan, 2)
272
273 #define PROTO_ENUM_ENTRY(name, number, ...) name = number,
274 enum class proto_type_t : uint8_t { PROTO_TYPES(PROTO_ENUM_ENTRY) };
275 enum class proto_error_t : uint8_t { none = 0, PROTO_ERRORS(PROTO_ENUM_ENTRY) };
276 enum class proto_feed_t : uint8_t { PROTO_FEEDS(PROTO_ENUM_ENTRY) };
277 #undef PROTO_ENUM_ENTRY
278 #define PROTO_FIELD_ENTRY(name, ...) name,
279 enum class proto_field_t : uint8_t { PROTO_FIELDS(PROTO_FIELD_ENTRY) count };
280 #undef PROTO_FIELD_ENTRY
281
282 struct proto_field_info_t {
283 const char *name;
284 const char *key;
285 char kind;
286 int32_t def;
287 };
288 struct proto_named_t {
289 uint8_t number;
290 const char *name;
291 char sender; // types only
292 char op; // types only
293 };
294 // What becomes of a client op, from the TYPE table.
295 enum class proto_op_t : char {
296 none = '-', // not an op
297 shadeRequest = 'q', // a command: a shade request loop() runs
298 remoteFrame = 'f', // sendRemoteCommand: a frame that goes to the radio at once
299 edit = 'e', // an edit loop() applies
300 read = 'r', // a read loop() answers from the shade model
301 device = 'd' // a settings, scan or reboot op loop() runs
302 };
303 const proto_field_info_t &protoField(proto_field_t field);
304 const proto_field_info_t *protoFieldByKey(const char *key);
305 // The field with that key; false when there is none.
306 bool protoFieldOfKey(const char *key, proto_field_t &out);
307 // The tables as the schema lists them, in declaration order.
308 uint8_t protoTypeCount();
309 const proto_named_t &protoTypeAt(uint8_t index);
310 uint8_t protoErrorCount();
311 const proto_named_t &protoErrorAt(uint8_t index);
312 uint8_t protoFeedCount();
313 const proto_named_t &protoFeedAt(uint8_t index);
314 const char *protoTypeName(uint8_t number);
315 // What a client may send: control messages it sends and the ops.
316 bool protoClientMaySend(uint8_t type);
317 proto_op_t protoOpKind(uint8_t type);
318 // Hex FNV-1a of every row of the schema: 8 characters and a terminator.
319 const char *protoSchemaHash();
320
321 // Writes fields by their short keys into the open object of json: booleans as
322 // 1/0, and nothing at all for a value at the field's default.
323 class ProtoWriter {
324 public:
325 51 explicit ProtoWriter(JsonFormatter &json) : json(json) {}
326 7179 template<typename T> void add(proto_field_t field, T value) {
327 7179 const proto_field_info_t &f = protoField(field);
328 if constexpr(std::is_same<T, bool>::value) {
329
4/4
✓ Branch 0 taken 1011 times.
✓ Branch 1 taken 147 times.
✓ Branch 2 taken 147 times.
✓ Branch 3 taken 1011 times.
1158 if((value ? 1 : 0) == f.def) return;
330
2/2
✓ Branch 0 taken 1010 times.
✓ Branch 1 taken 1 time.
1011 this->json.addElem(f.key, (uint8_t)(value ? 1 : 0));
331 }
332 else if constexpr(std::is_floating_point<T>::value) {
333 if(value == (T)f.def) return;
334 this->json.addElem(f.key, (float)value);
335 }
336 else if constexpr(std::is_integral<T>::value) {
337 5779 this->addInt(f, (int64_t)value);
338 }
339 else {
340 242 const char *s = value;
341
4/4
✓ Branch 0 taken 241 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 234 times.
242 if(!s || !s[0]) return;
342 234 this->json.addElem(f.key, s);
343 }
344 5779 }
345 349 void beginList(proto_field_t field) { this->json.beginArray(protoField(field).key); }
346 235 void endList() { this->json.endArray(); }
347 12 void beginObject(proto_field_t field) { this->json.beginObject(protoField(field).key); }
348 12 void endObject() { this->json.endObject(); }
349 // An object or a value inside a list.
350 1116 void beginItem() { this->json.beginObject(); }
351 1116 void endItem() { this->json.endObject(); }
352 void item(int64_t value);
353 JsonFormatter &json;
354 private:
355 void addInt(const proto_field_info_t &f, int64_t value);
356 };
357 #endif
358