├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── docs
└── protocol.md
└── src
├── barcode.rs
├── capture.rs
├── config.rs
├── gateway.rs
├── gateway
├── link.rs
├── link
│ ├── address.rs
│ ├── crc.rs
│ ├── escaping.rs
│ └── receive.rs
├── physical.rs
├── physical
│ ├── serialport.rs
│ ├── tcp.rs
│ ├── termios.rs
│ ├── trace_meshdcd.rs
│ └── trace_meshdcd
│ │ ├── target.rs
│ │ └── traced_process.rs
├── transport.rs
└── transport
│ └── receiver.rs
├── lib.rs
├── main.rs
├── observer.rs
├── observer
├── event.rs
├── node_table.rs
├── slot_clock.rs
└── tests.rs
├── pv.rs
├── pv
├── application.rs
├── application
│ ├── node_table.rs
│ ├── packet_type.rs
│ ├── power_report.rs
│ ├── receiver.rs
│ ├── string.rs
│ └── topology_report.rs
├── link.rs
├── link
│ └── slot_counter.rs
├── network.rs
└── physical.rs
└── test_data.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "adler"
7 | version = "1.0.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
10 |
11 | [[package]]
12 | name = "aho-corasick"
13 | version = "1.1.3"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
16 | dependencies = [
17 | "memchr",
18 | ]
19 |
20 | [[package]]
21 | name = "android-tzdata"
22 | version = "0.1.1"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
25 |
26 | [[package]]
27 | name = "android_system_properties"
28 | version = "0.1.5"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
31 | dependencies = [
32 | "libc",
33 | ]
34 |
35 | [[package]]
36 | name = "anstream"
37 | version = "0.6.15"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
40 | dependencies = [
41 | "anstyle",
42 | "anstyle-parse",
43 | "anstyle-query",
44 | "anstyle-wincon",
45 | "colorchoice",
46 | "is_terminal_polyfill",
47 | "utf8parse",
48 | ]
49 |
50 | [[package]]
51 | name = "anstyle"
52 | version = "1.0.8"
53 | source = "registry+https://github.com/rust-lang/crates.io-index"
54 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
55 |
56 | [[package]]
57 | name = "anstyle-parse"
58 | version = "0.2.5"
59 | source = "registry+https://github.com/rust-lang/crates.io-index"
60 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
61 | dependencies = [
62 | "utf8parse",
63 | ]
64 |
65 | [[package]]
66 | name = "anstyle-query"
67 | version = "1.1.1"
68 | source = "registry+https://github.com/rust-lang/crates.io-index"
69 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
70 | dependencies = [
71 | "windows-sys",
72 | ]
73 |
74 | [[package]]
75 | name = "anstyle-wincon"
76 | version = "3.0.4"
77 | source = "registry+https://github.com/rust-lang/crates.io-index"
78 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
79 | dependencies = [
80 | "anstyle",
81 | "windows-sys",
82 | ]
83 |
84 | [[package]]
85 | name = "autocfg"
86 | version = "1.3.0"
87 | source = "registry+https://github.com/rust-lang/crates.io-index"
88 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
89 |
90 | [[package]]
91 | name = "bitflags"
92 | version = "1.3.2"
93 | source = "registry+https://github.com/rust-lang/crates.io-index"
94 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
95 |
96 | [[package]]
97 | name = "bitflags"
98 | version = "2.6.0"
99 | source = "registry+https://github.com/rust-lang/crates.io-index"
100 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
101 |
102 | [[package]]
103 | name = "bumpalo"
104 | version = "3.16.0"
105 | source = "registry+https://github.com/rust-lang/crates.io-index"
106 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
107 |
108 | [[package]]
109 | name = "cc"
110 | version = "1.1.6"
111 | source = "registry+https://github.com/rust-lang/crates.io-index"
112 | checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
113 |
114 | [[package]]
115 | name = "cfg-if"
116 | version = "1.0.0"
117 | source = "registry+https://github.com/rust-lang/crates.io-index"
118 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
119 |
120 | [[package]]
121 | name = "chrono"
122 | version = "0.4.38"
123 | source = "registry+https://github.com/rust-lang/crates.io-index"
124 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
125 | dependencies = [
126 | "android-tzdata",
127 | "iana-time-zone",
128 | "js-sys",
129 | "num-traits",
130 | "serde",
131 | "wasm-bindgen",
132 | "windows-targets",
133 | ]
134 |
135 | [[package]]
136 | name = "clap"
137 | version = "4.5.13"
138 | source = "registry+https://github.com/rust-lang/crates.io-index"
139 | checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
140 | dependencies = [
141 | "clap_builder",
142 | "clap_derive",
143 | ]
144 |
145 | [[package]]
146 | name = "clap_builder"
147 | version = "4.5.13"
148 | source = "registry+https://github.com/rust-lang/crates.io-index"
149 | checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
150 | dependencies = [
151 | "anstream",
152 | "anstyle",
153 | "clap_lex",
154 | "strsim",
155 | ]
156 |
157 | [[package]]
158 | name = "clap_derive"
159 | version = "4.5.13"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
162 | dependencies = [
163 | "heck",
164 | "proc-macro2",
165 | "quote",
166 | "syn",
167 | ]
168 |
169 | [[package]]
170 | name = "clap_lex"
171 | version = "0.7.2"
172 | source = "registry+https://github.com/rust-lang/crates.io-index"
173 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
174 |
175 | [[package]]
176 | name = "colorchoice"
177 | version = "1.0.2"
178 | source = "registry+https://github.com/rust-lang/crates.io-index"
179 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
180 |
181 | [[package]]
182 | name = "core-foundation-sys"
183 | version = "0.8.6"
184 | source = "registry+https://github.com/rust-lang/crates.io-index"
185 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
186 |
187 | [[package]]
188 | name = "crc32fast"
189 | version = "1.4.2"
190 | source = "registry+https://github.com/rust-lang/crates.io-index"
191 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
192 | dependencies = [
193 | "cfg-if",
194 | ]
195 |
196 | [[package]]
197 | name = "dyn-clone"
198 | version = "1.0.17"
199 | source = "registry+https://github.com/rust-lang/crates.io-index"
200 | checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
201 |
202 | [[package]]
203 | name = "env_filter"
204 | version = "0.1.2"
205 | source = "registry+https://github.com/rust-lang/crates.io-index"
206 | checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
207 | dependencies = [
208 | "log",
209 | "regex",
210 | ]
211 |
212 | [[package]]
213 | name = "env_logger"
214 | version = "0.11.5"
215 | source = "registry+https://github.com/rust-lang/crates.io-index"
216 | checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
217 | dependencies = [
218 | "anstream",
219 | "anstyle",
220 | "env_filter",
221 | "humantime",
222 | "log",
223 | ]
224 |
225 | [[package]]
226 | name = "flate2"
227 | version = "1.0.31"
228 | source = "registry+https://github.com/rust-lang/crates.io-index"
229 | checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920"
230 | dependencies = [
231 | "crc32fast",
232 | "miniz_oxide",
233 | ]
234 |
235 | [[package]]
236 | name = "heck"
237 | version = "0.5.0"
238 | source = "registry+https://github.com/rust-lang/crates.io-index"
239 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
240 |
241 | [[package]]
242 | name = "humantime"
243 | version = "2.1.0"
244 | source = "registry+https://github.com/rust-lang/crates.io-index"
245 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
246 |
247 | [[package]]
248 | name = "iana-time-zone"
249 | version = "0.1.60"
250 | source = "registry+https://github.com/rust-lang/crates.io-index"
251 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
252 | dependencies = [
253 | "android_system_properties",
254 | "core-foundation-sys",
255 | "iana-time-zone-haiku",
256 | "js-sys",
257 | "wasm-bindgen",
258 | "windows-core",
259 | ]
260 |
261 | [[package]]
262 | name = "iana-time-zone-haiku"
263 | version = "0.1.2"
264 | source = "registry+https://github.com/rust-lang/crates.io-index"
265 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
266 | dependencies = [
267 | "cc",
268 | ]
269 |
270 | [[package]]
271 | name = "io-kit-sys"
272 | version = "0.4.1"
273 | source = "registry+https://github.com/rust-lang/crates.io-index"
274 | checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
275 | dependencies = [
276 | "core-foundation-sys",
277 | "mach2",
278 | ]
279 |
280 | [[package]]
281 | name = "is_terminal_polyfill"
282 | version = "1.70.1"
283 | source = "registry+https://github.com/rust-lang/crates.io-index"
284 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
285 |
286 | [[package]]
287 | name = "itoa"
288 | version = "1.0.11"
289 | source = "registry+https://github.com/rust-lang/crates.io-index"
290 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
291 |
292 | [[package]]
293 | name = "js-sys"
294 | version = "0.3.69"
295 | source = "registry+https://github.com/rust-lang/crates.io-index"
296 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
297 | dependencies = [
298 | "wasm-bindgen",
299 | ]
300 |
301 | [[package]]
302 | name = "libc"
303 | version = "0.2.155"
304 | source = "registry+https://github.com/rust-lang/crates.io-index"
305 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
306 |
307 | [[package]]
308 | name = "libudev"
309 | version = "0.3.0"
310 | source = "registry+https://github.com/rust-lang/crates.io-index"
311 | checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0"
312 | dependencies = [
313 | "libc",
314 | "libudev-sys",
315 | ]
316 |
317 | [[package]]
318 | name = "libudev-sys"
319 | version = "0.1.4"
320 | source = "registry+https://github.com/rust-lang/crates.io-index"
321 | checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
322 | dependencies = [
323 | "libc",
324 | "pkg-config",
325 | ]
326 |
327 | [[package]]
328 | name = "log"
329 | version = "0.4.22"
330 | source = "registry+https://github.com/rust-lang/crates.io-index"
331 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
332 |
333 | [[package]]
334 | name = "mach2"
335 | version = "0.4.2"
336 | source = "registry+https://github.com/rust-lang/crates.io-index"
337 | checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
338 | dependencies = [
339 | "libc",
340 | ]
341 |
342 | [[package]]
343 | name = "memchr"
344 | version = "2.7.4"
345 | source = "registry+https://github.com/rust-lang/crates.io-index"
346 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
347 |
348 | [[package]]
349 | name = "miniz_oxide"
350 | version = "0.7.4"
351 | source = "registry+https://github.com/rust-lang/crates.io-index"
352 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
353 | dependencies = [
354 | "adler",
355 | ]
356 |
357 | [[package]]
358 | name = "nix"
359 | version = "0.26.4"
360 | source = "registry+https://github.com/rust-lang/crates.io-index"
361 | checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
362 | dependencies = [
363 | "bitflags 1.3.2",
364 | "cfg-if",
365 | "libc",
366 | ]
367 |
368 | [[package]]
369 | name = "num-traits"
370 | version = "0.2.19"
371 | source = "registry+https://github.com/rust-lang/crates.io-index"
372 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
373 | dependencies = [
374 | "autocfg",
375 | ]
376 |
377 | [[package]]
378 | name = "once_cell"
379 | version = "1.19.0"
380 | source = "registry+https://github.com/rust-lang/crates.io-index"
381 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
382 |
383 | [[package]]
384 | name = "pkg-config"
385 | version = "0.3.30"
386 | source = "registry+https://github.com/rust-lang/crates.io-index"
387 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
388 |
389 | [[package]]
390 | name = "proc-macro2"
391 | version = "1.0.86"
392 | source = "registry+https://github.com/rust-lang/crates.io-index"
393 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
394 | dependencies = [
395 | "unicode-ident",
396 | ]
397 |
398 | [[package]]
399 | name = "quote"
400 | version = "1.0.36"
401 | source = "registry+https://github.com/rust-lang/crates.io-index"
402 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
403 | dependencies = [
404 | "proc-macro2",
405 | ]
406 |
407 | [[package]]
408 | name = "ref-cast"
409 | version = "1.0.23"
410 | source = "registry+https://github.com/rust-lang/crates.io-index"
411 | checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931"
412 | dependencies = [
413 | "ref-cast-impl",
414 | ]
415 |
416 | [[package]]
417 | name = "ref-cast-impl"
418 | version = "1.0.23"
419 | source = "registry+https://github.com/rust-lang/crates.io-index"
420 | checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
421 | dependencies = [
422 | "proc-macro2",
423 | "quote",
424 | "syn",
425 | ]
426 |
427 | [[package]]
428 | name = "regex"
429 | version = "1.10.5"
430 | source = "registry+https://github.com/rust-lang/crates.io-index"
431 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
432 | dependencies = [
433 | "aho-corasick",
434 | "memchr",
435 | "regex-automata",
436 | "regex-syntax",
437 | ]
438 |
439 | [[package]]
440 | name = "regex-automata"
441 | version = "0.4.7"
442 | source = "registry+https://github.com/rust-lang/crates.io-index"
443 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
444 | dependencies = [
445 | "aho-corasick",
446 | "memchr",
447 | "regex-syntax",
448 | ]
449 |
450 | [[package]]
451 | name = "regex-syntax"
452 | version = "0.8.4"
453 | source = "registry+https://github.com/rust-lang/crates.io-index"
454 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
455 |
456 | [[package]]
457 | name = "ryu"
458 | version = "1.0.18"
459 | source = "registry+https://github.com/rust-lang/crates.io-index"
460 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
461 |
462 | [[package]]
463 | name = "schemars"
464 | version = "1.0.0-alpha.2"
465 | source = "registry+https://github.com/rust-lang/crates.io-index"
466 | checksum = "ec9b1e7918a904d86cb6de7147ff4da21f12ac1462c8049e12b30a8846f4699e"
467 | dependencies = [
468 | "chrono",
469 | "dyn-clone",
470 | "ref-cast",
471 | "schemars_derive",
472 | "serde",
473 | "serde_json",
474 | ]
475 |
476 | [[package]]
477 | name = "schemars_derive"
478 | version = "1.0.0-alpha.2"
479 | source = "registry+https://github.com/rust-lang/crates.io-index"
480 | checksum = "e2730d5d2dbaf504ab238832cad00b0bdd727436583c7b05f9328e65fee2b475"
481 | dependencies = [
482 | "proc-macro2",
483 | "quote",
484 | "serde_derive_internals",
485 | "syn",
486 | ]
487 |
488 | [[package]]
489 | name = "scopeguard"
490 | version = "1.2.0"
491 | source = "registry+https://github.com/rust-lang/crates.io-index"
492 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
493 |
494 | [[package]]
495 | name = "serde"
496 | version = "1.0.204"
497 | source = "registry+https://github.com/rust-lang/crates.io-index"
498 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
499 | dependencies = [
500 | "serde_derive",
501 | ]
502 |
503 | [[package]]
504 | name = "serde_derive"
505 | version = "1.0.204"
506 | source = "registry+https://github.com/rust-lang/crates.io-index"
507 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
508 | dependencies = [
509 | "proc-macro2",
510 | "quote",
511 | "syn",
512 | ]
513 |
514 | [[package]]
515 | name = "serde_derive_internals"
516 | version = "0.29.1"
517 | source = "registry+https://github.com/rust-lang/crates.io-index"
518 | checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
519 | dependencies = [
520 | "proc-macro2",
521 | "quote",
522 | "syn",
523 | ]
524 |
525 | [[package]]
526 | name = "serde_json"
527 | version = "1.0.122"
528 | source = "registry+https://github.com/rust-lang/crates.io-index"
529 | checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
530 | dependencies = [
531 | "itoa",
532 | "memchr",
533 | "ryu",
534 | "serde",
535 | ]
536 |
537 | [[package]]
538 | name = "serialport"
539 | version = "4.4.0"
540 | source = "registry+https://github.com/rust-lang/crates.io-index"
541 | checksum = "de7c4f0cce25b9b3518eea99618112f9ee4549f974480c8f43d3c06f03c131a0"
542 | dependencies = [
543 | "bitflags 2.6.0",
544 | "cfg-if",
545 | "core-foundation-sys",
546 | "io-kit-sys",
547 | "libudev",
548 | "mach2",
549 | "nix",
550 | "regex",
551 | "scopeguard",
552 | "unescaper",
553 | "winapi",
554 | ]
555 |
556 | [[package]]
557 | name = "strsim"
558 | version = "0.11.1"
559 | source = "registry+https://github.com/rust-lang/crates.io-index"
560 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
561 |
562 | [[package]]
563 | name = "syn"
564 | version = "2.0.71"
565 | source = "registry+https://github.com/rust-lang/crates.io-index"
566 | checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
567 | dependencies = [
568 | "proc-macro2",
569 | "quote",
570 | "unicode-ident",
571 | ]
572 |
573 | [[package]]
574 | name = "taptap"
575 | version = "0.1.1"
576 | dependencies = [
577 | "chrono",
578 | "clap",
579 | "env_logger",
580 | "flate2",
581 | "libc",
582 | "log",
583 | "schemars",
584 | "serde",
585 | "serde_json",
586 | "serialport",
587 | "thiserror",
588 | "zerocopy",
589 | ]
590 |
591 | [[package]]
592 | name = "thiserror"
593 | version = "1.0.62"
594 | source = "registry+https://github.com/rust-lang/crates.io-index"
595 | checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
596 | dependencies = [
597 | "thiserror-impl",
598 | ]
599 |
600 | [[package]]
601 | name = "thiserror-impl"
602 | version = "1.0.62"
603 | source = "registry+https://github.com/rust-lang/crates.io-index"
604 | checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
605 | dependencies = [
606 | "proc-macro2",
607 | "quote",
608 | "syn",
609 | ]
610 |
611 | [[package]]
612 | name = "unescaper"
613 | version = "0.1.5"
614 | source = "registry+https://github.com/rust-lang/crates.io-index"
615 | checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815"
616 | dependencies = [
617 | "thiserror",
618 | ]
619 |
620 | [[package]]
621 | name = "unicode-ident"
622 | version = "1.0.12"
623 | source = "registry+https://github.com/rust-lang/crates.io-index"
624 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
625 |
626 | [[package]]
627 | name = "utf8parse"
628 | version = "0.2.2"
629 | source = "registry+https://github.com/rust-lang/crates.io-index"
630 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
631 |
632 | [[package]]
633 | name = "wasm-bindgen"
634 | version = "0.2.92"
635 | source = "registry+https://github.com/rust-lang/crates.io-index"
636 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
637 | dependencies = [
638 | "cfg-if",
639 | "wasm-bindgen-macro",
640 | ]
641 |
642 | [[package]]
643 | name = "wasm-bindgen-backend"
644 | version = "0.2.92"
645 | source = "registry+https://github.com/rust-lang/crates.io-index"
646 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
647 | dependencies = [
648 | "bumpalo",
649 | "log",
650 | "once_cell",
651 | "proc-macro2",
652 | "quote",
653 | "syn",
654 | "wasm-bindgen-shared",
655 | ]
656 |
657 | [[package]]
658 | name = "wasm-bindgen-macro"
659 | version = "0.2.92"
660 | source = "registry+https://github.com/rust-lang/crates.io-index"
661 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
662 | dependencies = [
663 | "quote",
664 | "wasm-bindgen-macro-support",
665 | ]
666 |
667 | [[package]]
668 | name = "wasm-bindgen-macro-support"
669 | version = "0.2.92"
670 | source = "registry+https://github.com/rust-lang/crates.io-index"
671 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
672 | dependencies = [
673 | "proc-macro2",
674 | "quote",
675 | "syn",
676 | "wasm-bindgen-backend",
677 | "wasm-bindgen-shared",
678 | ]
679 |
680 | [[package]]
681 | name = "wasm-bindgen-shared"
682 | version = "0.2.92"
683 | source = "registry+https://github.com/rust-lang/crates.io-index"
684 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
685 |
686 | [[package]]
687 | name = "winapi"
688 | version = "0.3.9"
689 | source = "registry+https://github.com/rust-lang/crates.io-index"
690 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
691 | dependencies = [
692 | "winapi-i686-pc-windows-gnu",
693 | "winapi-x86_64-pc-windows-gnu",
694 | ]
695 |
696 | [[package]]
697 | name = "winapi-i686-pc-windows-gnu"
698 | version = "0.4.0"
699 | source = "registry+https://github.com/rust-lang/crates.io-index"
700 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
701 |
702 | [[package]]
703 | name = "winapi-x86_64-pc-windows-gnu"
704 | version = "0.4.0"
705 | source = "registry+https://github.com/rust-lang/crates.io-index"
706 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
707 |
708 | [[package]]
709 | name = "windows-core"
710 | version = "0.52.0"
711 | source = "registry+https://github.com/rust-lang/crates.io-index"
712 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
713 | dependencies = [
714 | "windows-targets",
715 | ]
716 |
717 | [[package]]
718 | name = "windows-sys"
719 | version = "0.52.0"
720 | source = "registry+https://github.com/rust-lang/crates.io-index"
721 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
722 | dependencies = [
723 | "windows-targets",
724 | ]
725 |
726 | [[package]]
727 | name = "windows-targets"
728 | version = "0.52.6"
729 | source = "registry+https://github.com/rust-lang/crates.io-index"
730 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
731 | dependencies = [
732 | "windows_aarch64_gnullvm",
733 | "windows_aarch64_msvc",
734 | "windows_i686_gnu",
735 | "windows_i686_gnullvm",
736 | "windows_i686_msvc",
737 | "windows_x86_64_gnu",
738 | "windows_x86_64_gnullvm",
739 | "windows_x86_64_msvc",
740 | ]
741 |
742 | [[package]]
743 | name = "windows_aarch64_gnullvm"
744 | version = "0.52.6"
745 | source = "registry+https://github.com/rust-lang/crates.io-index"
746 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
747 |
748 | [[package]]
749 | name = "windows_aarch64_msvc"
750 | version = "0.52.6"
751 | source = "registry+https://github.com/rust-lang/crates.io-index"
752 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
753 |
754 | [[package]]
755 | name = "windows_i686_gnu"
756 | version = "0.52.6"
757 | source = "registry+https://github.com/rust-lang/crates.io-index"
758 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
759 |
760 | [[package]]
761 | name = "windows_i686_gnullvm"
762 | version = "0.52.6"
763 | source = "registry+https://github.com/rust-lang/crates.io-index"
764 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
765 |
766 | [[package]]
767 | name = "windows_i686_msvc"
768 | version = "0.52.6"
769 | source = "registry+https://github.com/rust-lang/crates.io-index"
770 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
771 |
772 | [[package]]
773 | name = "windows_x86_64_gnu"
774 | version = "0.52.6"
775 | source = "registry+https://github.com/rust-lang/crates.io-index"
776 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
777 |
778 | [[package]]
779 | name = "windows_x86_64_gnullvm"
780 | version = "0.52.6"
781 | source = "registry+https://github.com/rust-lang/crates.io-index"
782 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
783 |
784 | [[package]]
785 | name = "windows_x86_64_msvc"
786 | version = "0.52.6"
787 | source = "registry+https://github.com/rust-lang/crates.io-index"
788 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
789 |
790 | [[package]]
791 | name = "zerocopy"
792 | version = "0.8.0-alpha.16"
793 | source = "registry+https://github.com/rust-lang/crates.io-index"
794 | checksum = "0a5fe242a39bc4f8b8d808be6314c0f0e5e499a902c44e704f3c86a89f7a7c64"
795 | dependencies = [
796 | "zerocopy-derive",
797 | ]
798 |
799 | [[package]]
800 | name = "zerocopy-derive"
801 | version = "0.8.0-alpha.16"
802 | source = "registry+https://github.com/rust-lang/crates.io-index"
803 | checksum = "76fc519c421ad48c6c8ba02cee449398d54276c839887f9f3562d1862b43b91c"
804 | dependencies = [
805 | "proc-macro2",
806 | "quote",
807 | "syn",
808 | ]
809 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "taptap"
3 | description = "An implementation of the Tigo TAP protocol"
4 | authors = ["Will Glynn"]
5 | repository = "https://github.com/willglynn/taptap"
6 | keywords = ["solar"]
7 | readme = "README.md"
8 | version = "0.1.1"
9 | edition = "2021"
10 | license = "MIT"
11 |
12 | [features]
13 | default = ["serialport", "clap", "env_logger"]
14 |
15 | [dependencies]
16 | # Library dependencies
17 | thiserror = "1.0"
18 | serde = { version = "1.0", features = ["derive"] }
19 | serde_json = "1.0"
20 | schemars = { version = "1.0.0-alpha.2", features = ["chrono04"] }
21 | chrono = { version = "0.4.38", features = ["serde"] }
22 | libc = "0.2.155"
23 | zerocopy = { version = "0.8.0-alpha.16", features = ["derive"] }
24 | flate2 = "1.0"
25 | log = "0.4.22"
26 |
27 | # Optional library features
28 | serialport = { version = "4.4", optional = true }
29 |
30 | # Executable dependencies
31 | clap = { version = "4.5.13", features = ["derive"], optional = true }
32 | env_logger = { version = "0.11.5", optional = true }
33 |
34 | [[bin]]
35 | name = "taptap"
36 | required-features = ["clap", "env_logger"]
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Will Glynn.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `taptap`
2 |
3 | This project implements the [Tigo TAP](https://www.tigoenergy.com/product/tigo-access-point) protocol, especially for
4 | the purpose of monitoring a Tigo TAP and the associated solar array using the TAP's communication cable. This allows
5 | 100% local offline data collection.
6 |
7 | The TAP protocol is described at [`docs/protocol.md`](https://github.com/willglynn/taptap/blob/master/docs/protocol.md).
8 | This system uses two networks, a wired "gateway network" and a wireless "PV network":
9 |
10 | ```text
11 | Gateway PV device
12 | device (TAP) (optimizer)
13 | ┌─────────────────┐ ┌─────────────────┐
14 | PV ┌─▶│ Application │ │ Application │ Proprietary
15 | network │ ├─────────────────┤ ├─────────────────┤ │
16 | │ │ Network │ │ Network │ │
17 | │ ├─────────────────┤ ├─────────────────┤
18 | │ │ Link │ │ Link │ 802.15.4
19 | │ ├─────────────────┤ ├─────────────────┤ │
20 | │ │ Physical │ │ Physical │ │
21 | │ └─────────────────┘ └─────────────────┘
22 | │ ▲ ▲
23 | │ └ ─ ─ ─ ─ ┘
24 | │ ┌─────────────────┐
25 | Gateway └─▶│ Transport │ Proprietary
26 | network ├─────────────────┤ │
27 | │ Link │ │
28 | ├─────────────────┤
29 | │ Physical │ RS-485
30 | └─────────────────┘
31 | ```
32 |
33 | ## Connecting
34 |
35 | The gateway network runs over RS-485 and can support more than two connections. An owner may therefore connect a USB
36 | RS-485 adapter, or an RS-485 hat, or any other RS-485 interface without interrupting communication.
37 |
38 | The gateway network supports a single controller. Most owners use a Tigo Cloud Connect Advanced (CCA), but there are
39 | alternatives, including older Tigo products and similar controllers embedded in GoodWe inverters. `taptap` can observe
40 | the controller's communication, without ever transmitting anything; as far as the other components are concerned, it
41 | does not exist. This allows owners to gather real-time information from their own hardware without going through Tigo's
42 | cloud platform and without modifying the controller, their TAPs, or any other hardware in any way.
43 |
44 |
45 | Placement considerations
46 | This system uses a 4-wire bus: ground (– or ⏚), power (+), A, and B. These wires are intended to run from the
47 | controller to a TAP, and possibly to another TAP, and so on. The A and B wires carry RS-485 signals. Tigo recommends
48 | putting a 120Ω resistor on the last TAP's A and B wires to terminate the far end of the bus, and they built a 120Ω
49 | resistor into the controller to terminate the near end of the bus.
50 |
51 | If you are adding a monitoring device to an existing install, it would be best to move the controller's A and B wires
52 | to the monitoring device, and then to run new wires from there to the controller. Having said that, it should be fine to
53 | connect short wires from the controller's A and B terminals to the monitoring device, especially if you plan never to
54 | transmit. (Your monitoring device may also have a "ground" or "reference" terminal, which should go to the controller's
55 | gateway ⏚ ground.) In either case, make sure the RS-485 interface you're adding does not include a third termination
56 | resistor. The bus should always be terminated at the controller and at the furthest away TAP.
57 |
58 | ```text
59 | ┌─────────────────────────────────────┐ ┌────────────────────────────┐
60 | │ CCA │ │ TAP │
61 | │ │ │ │
62 | │ AUX RS485-1 GATEWAY RS485-2 POWER│ │ ┌~┐ │
63 | │┌─┬─┐ ┌─┬─┬─┐ ┌─┬─┬─┬─┐ ┌─┬─┬─┐ ┌─┬─┐│ │ ┌─┬─┬─┬─┐ ┌─┬─┬│┬│┐ │
64 | ││/│_│ │-│B│A│ │-│+│B│A│ │-│B│A│ │-│+││ │ │-│+│B│A│ │-│+│B│A│ │
65 | │└─┴─┘ └─┴─┴─┘ └│┴│┴│┴│┘ └─┴─┴─┘ └─┴─┘│ │ └│┴│┴│┴│┘ └─┴─┴─┴─┘ │
66 | └───────────────│─│─│─│───────────────┘ └────│─│─│─│─────────────────┘
67 | │ │ │ │ │ │ │ │
68 | │ │ │ ┃───────────────────────────│─│─│─┘
69 | │ │ ┃─┃───────────────────────────│─│─┘
70 | │ └─┃─┃───────────────────────────│─┘
71 | ┃───┃─┃───────────────────────────┘
72 | ┗━┓ ┃ ┃
73 | ┌───┃─┃─┃───┐
74 | │ ┌┃┬┃┬┃┐ │
75 | │ │-│B│A│ │
76 | │ └─┴─┴─┘ │
77 | │ Monitor │
78 | └───────────┘
79 | ```
80 |
81 |
82 |
83 |
84 | Future work: controller-less operation
85 | In the absence of another controller, taptap
could request PV packets from the gateway(s) itself. The
86 | gateway and PV modules appear to function autonomously after configuration, so for a fully commissioned system,
87 | receiving PV packets from the gateway without ever transmitting anything to the modules would likely be sufficient for
88 | monitoring.
89 |
90 |
91 |
92 | Software-based connection method for owners with root
access on their controller
93 | Some owners have root
access on their controller. These owners could install
94 | tcpserial_hook
on their controller to make the
95 | serial data available over the LAN, including to taptap
, without physically adding another RS-485
96 | interface.
97 | This method has several disadvantages: it requires root
access, it requires (reversibly) modifying the
98 | files on the controller, it might stop working in future firmware updates, it only works when the controller is working,
99 | etc. It is a fast way to get started for some users, but consider wiring in a separate RS-485 interface instead.
100 |
101 |
102 | ## Project structure
103 |
104 | `taptap` consists of a library and an executable. The executable is a CLI:
105 |
106 | ```console
107 | % taptap
108 | Usage: taptap
109 |
110 | Commands:
111 | observe Observe the system, extracting data as it runs
112 | list-serial-ports List `--serial` ports
113 | peek-bytes Peek at the raw data flowing at the gateway physical layer
114 | peek-frames Peek at the assembled frames at the gateway link layer
115 | peek-activity Peek at the gateway transport and PV application layer activity
116 | help Print this message or the help of the given subcommand(s)
117 |
118 | Options:
119 | -h, --help Print help
120 | -V, --version Print version
121 |
122 | % taptap observe --tcp 172.21.3.44
123 | {"gateway":{"id":4609},"node":{"id":116},"timestamp":"2024-08-24T09:16:41.686961-05:00","voltage_in":30.6,"voltage_out":30.2,"current":6.94,"dc_dc_duty_cycle":1.0,"temperature":26.8,"rssi":132}
124 | {"gateway":{"id":4609},"node":{"id":116},"timestamp":"2024-08-24T09:17:01.691683-05:00","voltage_in":30.75,"voltage_out":30.4,"current":6.895,"dc_dc_duty_cycle":1.0,"temperature":26.8,"rssi":132}
125 | {"gateway":{"id":4609},"node":{"id":82},"timestamp":"2024-08-24T09:16:41.686961-05:00","voltage_in":30.55,"voltage_out":30.2,"current":6.845,"dc_dc_duty_cycle":1.0,"temperature":29.3,"rssi":147}
126 | {"gateway":{"id":4609},"node":{"id":82},"timestamp":"2024-08-24T09:17:01.691683-05:00","voltage_in":30.95,"voltage_out":30.6,"current":6.765,"dc_dc_duty_cycle":1.0,"temperature":29.3,"rssi":147}
127 | {"gateway":{"id":4609},"node":{"id":19},"timestamp":"2024-08-24T09:16:41.686961-05:00","voltage_in":30.35,"voltage_out":29.9,"current":6.865,"dc_dc_duty_cycle":1.0,"temperature":28.7,"rssi":147}
128 | {"gateway":{"id":4609},"node":{"id":19},"timestamp":"2024-08-24T09:17:01.691683-05:00","voltage_in":29.85,"voltage_out":29.4,"current":7.005,"dc_dc_duty_cycle":1.0,"temperature":28.7,"rssi":147}
129 | {"gateway":{"id":4609},"node":{"id":121},"timestamp":"2024-08-24T09:16:41.686961-05:00","voltage_in":29.8,"voltage_out":21.9,"current":5.25,"dc_dc_duty_cycle":0.7607843137254902,"temperature":29.8,"rssi":120}
130 | {"gateway":{"id":4609},"node":{"id":121},"timestamp":"2024-08-24T09:17:01.691683-05:00","voltage_in":30.55,"voltage_out":22.8,"current":5.3,"dc_dc_duty_cycle":0.7725490196078432,"temperature":29.8,"rssi":120}
131 | ```
132 |
133 | As of this initial version, the `observe` subcommand emits `taptap::observer::Event`s to standard output as JSON rather
134 | than emitting metrics for InfluxDB or Prometheus, and it does not persist its own state, meaning the gateway and nodes
135 | are identified by their internal IDs rather than by barcode. These are the next two features to add.
136 |
--------------------------------------------------------------------------------
/src/barcode.rs:
--------------------------------------------------------------------------------
1 | use crate::pv;
2 | use schemars::JsonSchema;
3 | use serde::{Deserialize, Serialize};
4 | use std::fmt::Write;
5 |
6 | #[derive(
7 | Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema,
8 | )]
9 | #[serde(try_from = "String", into = "String")]
10 | pub struct Barcode(pub pv::LongAddress);
11 |
12 | const N2H: [u8; 16] = *b"0123456789ABCDEF";
13 |
14 | #[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
15 | #[error("invalid barcode: {0:?}")]
16 | pub struct InvalidBarcodeError(String);
17 |
18 | impl std::str::FromStr for Barcode {
19 | type Err = InvalidBarcodeError;
20 |
21 | fn from_str(s: &str) -> Result {
22 | if s.get(1..2) != Some("-") || s.len() < 5 {
23 | return Err(InvalidBarcodeError(s.into()));
24 | }
25 |
26 | let leading_nibble =
27 | u8::from_str_radix(&s[0..1], 16).map_err(|_| InvalidBarcodeError(s.into()))?;
28 | let (middle, checksum) = s.split_at(s.len() - 1);
29 | let (_, rest) = middle.split_at(2);
30 |
31 | let rest = u64::from_str_radix(rest, 16).map_err(|_| InvalidBarcodeError(s.into()))?;
32 |
33 | let addr = rest | (0x04c05b0 | leading_nibble as u64) << 36;
34 | let addr = pv::LongAddress(addr.to_be_bytes());
35 |
36 | if [crc(addr)] == checksum.as_bytes() {
37 | Ok(Barcode(addr))
38 | } else {
39 | Err(InvalidBarcodeError(s.into()))
40 | }
41 | }
42 | }
43 |
44 | impl std::fmt::Display for Barcode {
45 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
46 | let bytes = &self.0 .0;
47 |
48 | // Barcode formatting only applies to addresses with a certain prefix
49 | if bytes[0] != 0x04 || bytes[1] != 0xc0 || bytes[2] != 0x5b {
50 | return write!(f, "{}", self.0);
51 | }
52 |
53 | f.write_char(N2H[(bytes[3] >> 4) as usize] as char)?;
54 | f.write_char('-')?;
55 |
56 | let nibbles = [
57 | bytes[3] & 0xf,
58 | bytes[4] >> 4,
59 | bytes[4] & 0xf,
60 | bytes[5] >> 4,
61 | bytes[5] & 0xf,
62 | bytes[6] >> 4,
63 | bytes[6] & 0xf,
64 | bytes[7] >> 4,
65 | bytes[7] & 0xf,
66 | ];
67 |
68 | let mut skipping = true;
69 | for (i, nibble) in nibbles.iter().copied().enumerate() {
70 | match (skipping, nibble) {
71 | (true, 0) if i < 10 => {
72 | // Let it roll
73 | continue;
74 | }
75 | (true, _) => {
76 | // Stop skipping
77 | skipping = false;
78 | }
79 | _ => {}
80 | }
81 | f.write_char(N2H[nibble as usize] as char)?;
82 | }
83 |
84 | f.write_char(crc(self.0) as char)
85 | }
86 | }
87 |
88 | impl TryFrom for Barcode {
89 | type Error = InvalidBarcodeError;
90 |
91 | fn try_from(value: String) -> Result {
92 | value.parse()
93 | }
94 | }
95 |
96 | impl From for String {
97 | fn from(value: Barcode) -> Self {
98 | value.to_string()
99 | }
100 | }
101 |
102 | // https://stackoverflow.com/q/54507106/1026671
103 | // "it seems unlikely that anyone would use a CRC with so few bytes in a practical application"
104 | const TABLE: &[u8] = &[
105 | 0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9, 0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2, 0x5, 0x6, 0x3,
106 | 0x0, 0x9, 0xa, 0xf, 0xc, 0xe, 0xd, 0x8, 0xb, 0x2, 0x1, 0x4, 0x7, 0xa, 0x9, 0xc, 0xf, 0x6, 0x5,
107 | 0x0, 0x3, 0x1, 0x2, 0x7, 0x4, 0xd, 0xe, 0xb, 0x8, 0xf, 0xc, 0x9, 0xa, 0x3, 0x0, 0x5, 0x6, 0x4,
108 | 0x7, 0x2, 0x1, 0x8, 0xb, 0xe, 0xd, 0x7, 0x4, 0x1, 0x2, 0xb, 0x8, 0xd, 0xe, 0xc, 0xf, 0xa, 0x9,
109 | 0x0, 0x3, 0x6, 0x5, 0x2, 0x1, 0x4, 0x7, 0xe, 0xd, 0x8, 0xb, 0x9, 0xa, 0xf, 0xc, 0x5, 0x6, 0x3,
110 | 0x0, 0xd, 0xe, 0xb, 0x8, 0x1, 0x2, 0x7, 0x4, 0x6, 0x5, 0x0, 0x3, 0xa, 0x9, 0xc, 0xf, 0x8, 0xb,
111 | 0xe, 0xd, 0x4, 0x7, 0x2, 0x1, 0x3, 0x0, 0x5, 0x6, 0xf, 0xc, 0x9, 0xa, 0xe, 0xd, 0x8, 0xb, 0x2,
112 | 0x1, 0x4, 0x7, 0x5, 0x6, 0x3, 0x0, 0x9, 0xa, 0xf, 0xc, 0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2,
113 | 0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9, 0x4, 0x7, 0x2, 0x1, 0x8, 0xb, 0xe, 0xd, 0xf, 0xc, 0x9,
114 | 0xa, 0x3, 0x0, 0x5, 0x6, 0x1, 0x2, 0x7, 0x4, 0xd, 0xe, 0xb, 0x8, 0xa, 0x9, 0xc, 0xf, 0x6, 0x5,
115 | 0x0, 0x3, 0x9, 0xa, 0xf, 0xc, 0x5, 0x6, 0x3, 0x0, 0x2, 0x1, 0x4, 0x7, 0xe, 0xd, 0x8, 0xb, 0xc,
116 | 0xf, 0xa, 0x9, 0x0, 0x3, 0x6, 0x5, 0x7, 0x4, 0x1, 0x2, 0xb, 0x8, 0xd, 0xe, 0x3, 0x0, 0x5, 0x6,
117 | 0xf, 0xc, 0x9, 0xa, 0x8, 0xb, 0xe, 0xd, 0x4, 0x7, 0x2, 0x1, 0x6, 0x5, 0x0, 0x3, 0xa, 0x9, 0xc,
118 | 0xf, 0xd, 0xe, 0xb, 0x8, 0x1, 0x2, 0x7, 0x4,
119 | ];
120 |
121 | const C2H: [u8; 16] = *b"GHJKLMNPRSTVWXYZ";
122 |
123 | fn crc(addr: pv::LongAddress) -> u8 {
124 | let mut crc = 2;
125 | for byte in addr.0.as_slice() {
126 | crc = TABLE[(*byte ^ (crc << 4)) as usize];
127 | }
128 |
129 | C2H[crc as usize]
130 | }
131 |
132 | impl From<&Barcode> for pv::LongAddress {
133 | fn from(value: &Barcode) -> Self {
134 | value.0
135 | }
136 | }
137 | impl From for pv::LongAddress {
138 | fn from(value: Barcode) -> Self {
139 | value.0
140 | }
141 | }
142 | impl From for Barcode {
143 | fn from(value: pv::LongAddress) -> Self {
144 | Self(value)
145 | }
146 | }
147 | impl From<&pv::LongAddress> for Barcode {
148 | fn from(value: &pv::LongAddress) -> Self {
149 | Self(*value)
150 | }
151 | }
152 |
153 | #[cfg(test)]
154 | mod tests {
155 | use super::*;
156 | use std::str::FromStr;
157 |
158 | const ADDR: pv::LongAddress = pv::LongAddress([0x04, 0xC0, 0x5B, 0x40, 0x00, 0x9A, 0x57, 0xA2]);
159 | const BARCODE: &str = "4-9A57A2L";
160 |
161 | #[test]
162 | fn crc() {
163 | assert_eq!(
164 | super::crc(pv::LongAddress([
165 | 0x04, 0xC0, 0x5B, 0x40, 0x00, 0x9A, 0x57, 0xA2
166 | ])),
167 | 'L' as u8
168 | );
169 | assert_eq!(
170 | super::crc(pv::LongAddress([
171 | 0x04, 0xC0, 0x5B, 0x40, 0x00, 0x79, 0xAC, 0x16
172 | ])),
173 | 'V' as u8
174 | );
175 | assert_eq!(
176 | super::crc(pv::LongAddress([
177 | 0x04, 0xC0, 0x5B, 0x40, 0x00, 0x79, 0xAB, 0x99
178 | ])),
179 | 'W' as u8
180 | );
181 | }
182 |
183 | #[test]
184 | fn display() {
185 | assert_eq!(Barcode(ADDR).to_string(), BARCODE);
186 | }
187 |
188 | #[test]
189 | fn parse() {
190 | assert_eq!(Barcode::from_str("4-9A57A2L"), Ok(Barcode(ADDR)));
191 | assert!(Barcode::from_str("4-9A57A2G").is_err());
192 | }
193 |
194 | #[test]
195 | fn long_address_conversion() {
196 | assert_eq!(pv::LongAddress::from(Barcode(ADDR)), ADDR);
197 | assert_eq!(Barcode::from(ADDR), Barcode(ADDR));
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/capture.rs:
--------------------------------------------------------------------------------
1 | use std::io::ErrorKind::UnexpectedEof;
2 | use std::io::{BufReader, Read, Write};
3 | use std::mem::size_of;
4 | use std::ops::Add;
5 | use std::time::{Duration, SystemTime, UNIX_EPOCH};
6 | use zerocopy::{big_endian, FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
7 |
8 | const GZIP_HEADER_COMMENT: &[u8] = b"taptap capture";
9 |
10 | #[derive(Debug)]
11 | pub struct Reader(BufReader>>);
12 |
13 | impl Reader {
14 | pub fn new(reader: R) -> std::io::Result {
15 | let gz = flate2::bufread::GzDecoder::new(BufReader::new(reader));
16 | if let Some(h) = gz.header() {
17 | if h.comment() != Some(GZIP_HEADER_COMMENT) {
18 | // warn?
19 | }
20 | }
21 |
22 | Ok(Self(BufReader::new(gz)))
23 | }
24 | }
25 |
26 | impl Iterator for Reader {
27 | type Item = std::io::Result<(Vec, SystemTime)>;
28 |
29 | fn next(&mut self) -> Option {
30 | let mut record = [0u8; size_of::()];
31 | match self.0.read_exact(&mut record) {
32 | Err(e) if e.kind() == UnexpectedEof => {
33 | return None;
34 | }
35 | Err(e) => return Some(Err(e)),
36 | Ok(_) => {}
37 | };
38 |
39 | let record = Record::ref_from_bytes(&record).unwrap(); // infallible
40 | let mut data = vec![0; record.data_length.get() as usize];
41 | Some(match self.0.read_exact(&mut data) {
42 | Err(e) => Err(e),
43 | Ok(_) => Ok((data, record.timestamp())),
44 | })
45 | }
46 | }
47 |
48 | #[derive(Debug)]
49 | pub struct Writer(flate2::write::GzEncoder);
50 |
51 | impl Writer {
52 | pub fn new(writer: W) -> std::io::Result {
53 | let gz = flate2::GzBuilder::new()
54 | .comment(GZIP_HEADER_COMMENT)
55 | .write(writer, flate2::Compression::best());
56 | Ok(Self(gz))
57 | }
58 |
59 | pub fn write(&mut self, mut bytes: &[u8], timestamp: SystemTime) -> std::io::Result<()> {
60 | while bytes.len() > u16::MAX as usize {
61 | let (left, right) = bytes.split_at(u16::MAX as usize);
62 | self.write(left, timestamp)?;
63 | bytes = right;
64 | }
65 |
66 | assert!(bytes.len() <= u16::MAX as usize);
67 |
68 | let mut buffer = vec![0u8; bytes.len() + size_of::()];
69 | let (record, data) = buffer.as_mut_slice().split_at_mut(size_of::());
70 | let record = Record::mut_from_bytes(record).unwrap();
71 | record.set_timestamp(timestamp);
72 | record.data_length.set(bytes.len() as u16);
73 | data.copy_from_slice(bytes);
74 |
75 | self.0.write_all(&buffer)
76 | }
77 |
78 | pub fn flush(&mut self) -> std::io::Result<()> {
79 | self.0.flush()
80 | }
81 |
82 | pub fn finish(self) -> std::io::Result {
83 | self.0.finish()
84 | }
85 | }
86 |
87 | #[derive(
88 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable,
89 | )]
90 | #[repr(C)]
91 | struct Record {
92 | /// Number of data bytes in this block
93 | pub data_length: big_endian::U16,
94 | // Milliseconds since epoch
95 | pub timestamp: big_endian::U64,
96 | }
97 |
98 | impl Record {
99 | pub fn timestamp(&self) -> SystemTime {
100 | UNIX_EPOCH.add(Duration::from_millis(self.timestamp.get()))
101 | }
102 |
103 | pub fn set_timestamp(&mut self, timestamp: SystemTime) {
104 | self.timestamp
105 | .set(timestamp.duration_since(UNIX_EPOCH).unwrap().as_millis() as u64)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | use crate::gateway;
2 | use schemars::JsonSchema;
3 | use serde::{Deserialize, Serialize};
4 |
5 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
6 | #[serde(rename = "snake_case")]
7 | pub enum SourceConfig {
8 | #[cfg(feature = "serialport")]
9 | Serial(SerialSourceConfig),
10 | Tcp(TcpConnectionConfig),
11 | }
12 |
13 | impl SourceConfig {
14 | pub fn open(&self) -> Result, std::io::Error> {
15 | match self {
16 | #[cfg(feature = "serialport")]
17 | SourceConfig::Serial(config) => {
18 | let conn = gateway::physical::serialport::Port::open(&config.name)?;
19 | Ok(Box::new(conn))
20 | }
21 | SourceConfig::Tcp(config) => {
22 | let addr = (config.hostname.as_str(), config.port);
23 | let readonly = match config.mode {
24 | ConnectionMode::ReadWrite => false,
25 | ConnectionMode::ReadOnly => true,
26 | };
27 |
28 | let conn = gateway::physical::tcp::Connection::connect(addr, readonly)?;
29 | Ok(Box::new(conn))
30 | }
31 | }
32 | }
33 | }
34 |
35 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
36 | #[cfg(feature = "serialport")]
37 | pub struct SerialSourceConfig {
38 | pub name: String,
39 | }
40 | impl From for SourceConfig {
41 | fn from(value: SerialSourceConfig) -> Self {
42 | SourceConfig::Serial(value)
43 | }
44 | }
45 |
46 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
47 | pub struct TcpConnectionConfig {
48 | pub hostname: String,
49 | #[serde(default = "default_port")]
50 | pub port: u16,
51 | pub mode: ConnectionMode,
52 | }
53 | impl From for SourceConfig {
54 | fn from(value: TcpConnectionConfig) -> Self {
55 | Self::Tcp(value)
56 | }
57 | }
58 |
59 | fn default_port() -> u16 {
60 | 7160
61 | }
62 |
63 | #[derive(Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
64 | pub enum ConnectionMode {
65 | #[default]
66 | #[serde(rename = "readonly", alias = "ro")]
67 | ReadOnly,
68 | #[serde(rename = "readwrite", alias = "rw")]
69 | ReadWrite,
70 | }
71 |
--------------------------------------------------------------------------------
/src/gateway.rs:
--------------------------------------------------------------------------------
1 | //! An implementation of the gateway network.
2 | //!
3 | //! The gateway network consists of three layers, implemented in their own modules:
4 | //!
5 | //! * [`physical`]
6 | //! * [`link`]
7 | //! * [`transport`]
8 |
9 | pub mod physical;
10 |
11 | pub mod link;
12 | pub use link::{Frame, GatewayID};
13 |
14 | pub mod transport;
15 |
--------------------------------------------------------------------------------
/src/gateway/link.rs:
--------------------------------------------------------------------------------
1 | //! The gateway link layer.
2 |
3 | mod address;
4 |
5 | pub use address::{Address, GatewayID, InvalidGatewayID};
6 |
7 | mod crc;
8 |
9 | mod escaping;
10 | mod receive;
11 | pub use receive::{Counters, Receiver, Sink};
12 |
13 | /// A gateway link layer frame.
14 | #[derive(Debug, Clone, Eq, PartialEq)]
15 | pub struct Frame {
16 | pub address: Address,
17 | pub frame_type: Type,
18 | pub payload: Vec,
19 | }
20 |
21 | impl Frame {
22 | /// Encode the frame into `Bytes` ready for transmission by the physical layer, including a
23 | /// preamble.
24 | pub fn encode(&self) -> Vec {
25 | let start = match self.address {
26 | Address::From(_) => [0xff, 0x7e, 0x07].as_slice(),
27 | Address::To(_) => [0x00, 0xff, 0xff, 0x7e, 0x07].as_slice(),
28 | };
29 | let end = &[0x7e, 0x08];
30 |
31 | let mut output_buffer = Vec::with_capacity(
32 | start.len()
33 | + 4 // worst case escaped address
34 | + 4 // worst case escaped frame type
35 | + escaping::escaped_length(&self.payload)
36 | + 4 // worst case CRC
37 | + end.len(), // frame end
38 | );
39 | let initial_output_buffer_capacity = output_buffer.capacity();
40 |
41 | // Add the start sequence
42 | output_buffer.extend_from_slice(start);
43 |
44 | // Assemble the middle
45 | let mut body = Vec::with_capacity(2 + 2 + self.payload.len() + 2);
46 | let initial_body_capacity = body.capacity();
47 | body.extend_from_slice(&<[u8; 2]>::from(self.address));
48 | body.extend_from_slice(&self.frame_type.0.to_be_bytes());
49 | body.extend_from_slice(&self.payload);
50 |
51 | // Calculate and append the CRC
52 | let crc = crc::crc(&body);
53 | body.extend_from_slice(&crc.to_le_bytes());
54 |
55 | // Append the escaped content to the output buffer
56 | escaping::escape(&body, &mut output_buffer);
57 |
58 | // Append the terminator
59 | output_buffer.extend_from_slice(end);
60 |
61 | // Ensure we didn't need to reallocate
62 | debug_assert_eq!(body.capacity(), initial_body_capacity);
63 | debug_assert_eq!(output_buffer.capacity(), initial_output_buffer_capacity);
64 |
65 | // Ensure we didn't over-allocate
66 | debug_assert_eq!(body.len(), initial_body_capacity);
67 | debug_assert!(initial_output_buffer_capacity <= output_buffer.len() + 6);
68 |
69 | output_buffer
70 | }
71 | }
72 |
73 | /// A link layer frame type.
74 | #[derive(Copy, Clone, Eq, PartialEq)]
75 | pub struct Type(pub u16);
76 | impl Type {
77 | pub const RECEIVE_REQUEST: Self = Type(0x0148);
78 | pub const RECEIVE_RESPONSE: Self = Type(0x0149);
79 | pub const COMMAND_REQUEST: Self = Type(0x0B0F);
80 | pub const COMMAND_RESPONSE: Self = Type(0x0B10);
81 | pub const PING_REQUEST: Self = Type(0x0B00);
82 | pub const PING_RESPONSE: Self = Type(0x0B01);
83 | pub const ENUMERATION_START_REQUEST: Self = Type(0x0014);
84 | pub const ENUMERATION_START_RESPONSE: Self = Type(0x0015);
85 | pub const ENUMERATION_REQUEST: Self = Type(0x0038);
86 | pub const ENUMERATION_RESPONSE: Self = Type(0x0039);
87 | pub const ASSIGN_GATEWAY_ID_REQUEST: Self = Type(0x003C);
88 | pub const ASSIGN_GATEWAY_ID_RESPONSE: Self = Type(0x003D);
89 | pub const IDENTIFY_REQUEST: Self = Type(0x003A);
90 | pub const IDENTIFY_RESPONSE: Self = Type(0x003B);
91 | pub const VERSION_REQUEST: Self = Type(0x000A);
92 | pub const VERSION_RESPONSE: Self = Type(0x000B);
93 | pub const ENUMERATION_END_REQUEST: Self = Type(0x0E02);
94 | pub const ENUMERATION_END_RESPONSE: Self = Type(0x0006);
95 | }
96 |
97 | impl std::fmt::Debug for Type {
98 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 | match *self {
100 | Self::RECEIVE_REQUEST => f.write_str("Type::RECEIVE_REQUEST"),
101 | Self::RECEIVE_RESPONSE => f.write_str("Type::RECEIVE_RESPONSE"),
102 | Self::COMMAND_REQUEST => f.write_str("Type::COMMAND_REQUEST"),
103 | Self::COMMAND_RESPONSE => f.write_str("Type::COMMAND_RESPONSE"),
104 | Self::PING_REQUEST => f.write_str("Type::PING_REQUEST"),
105 | Self::PING_RESPONSE => f.write_str("Type::PING_RESPONSE"),
106 | Self::ENUMERATION_START_REQUEST => f.write_str("Type::ENUMERATION_START_REQUEST"),
107 | Self::ENUMERATION_START_RESPONSE => f.write_str("Type::ENUMERATION_START_RESPONSE"),
108 | Self::ENUMERATION_REQUEST => f.write_str("Type::ENUMERATION_REQUEST"),
109 | Self::ENUMERATION_RESPONSE => f.write_str("Type::ENUMERATION_RESPONSE"),
110 | Self::ASSIGN_GATEWAY_ID_REQUEST => f.write_str("Type::ASSIGN_GATEWAY_ID_REQUEST"),
111 | Self::ASSIGN_GATEWAY_ID_RESPONSE => f.write_str("Type::ASSIGN_GATEWAY_ID_RESPONSE"),
112 | Self::IDENTIFY_REQUEST => f.write_str("Type::IDENTIFY_REQUEST"),
113 | Self::IDENTIFY_RESPONSE => f.write_str("Type::IDENTIFY_RESPONSE"),
114 | Self::VERSION_REQUEST => f.write_str("Type::VERSION_REQUEST"),
115 | Self::VERSION_RESPONSE => f.write_str("Type::VERSION_RESPONSE"),
116 | Self::ENUMERATION_END_REQUEST => f.write_str("Type::ENUMERATION_END_REQUEST"),
117 | Self::ENUMERATION_END_RESPONSE => f.write_str("Type::ENUMERATION_END_RESPONSE"),
118 | Self(value) => f
119 | .debug_tuple("Type")
120 | .field(&format_args!("{:#04x}", value))
121 | .finish(),
122 | }
123 | }
124 | }
125 |
126 | #[cfg(test)]
127 | mod tests {
128 | use super::*;
129 |
130 | #[test]
131 | fn frame_encoding() {
132 | let encoded = Frame {
133 | address: Address::From(GatewayID::try_from(0x1201).unwrap()),
134 | frame_type: Type(0x0149),
135 | payload: b"\x00\xFF\x7C\xDB\xC2".as_slice().into(),
136 | }
137 | .encode();
138 |
139 | assert_eq!(
140 | encoded.as_slice(),
141 | [
142 | 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x01, 0x49, 0x00, 0xFF, 0x7C, 0xDB, 0xC2, 0x7E, 0x05,
143 | 0x85, 0x7E, 0x08
144 | ]
145 | .as_slice()
146 | );
147 |
148 | assert!(encoded.capacity() <= encoded.len() + 6);
149 | }
150 |
151 | #[test]
152 | fn type_debug() {
153 | assert_eq!(
154 | format!("{:?}", &Type::RECEIVE_RESPONSE),
155 | "Type::RECEIVE_RESPONSE"
156 | );
157 | assert_eq!(format!("{:?}", &Type(0x1234)), "Type(0x1234)");
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/gateway/link/address.rs:
--------------------------------------------------------------------------------
1 | use schemars::JsonSchema;
2 | use serde::de::Error;
3 | use serde::{Deserialize, Deserializer, Serialize};
4 | use std::convert::TryFrom;
5 |
6 | const DIRECTION_BIT: u16 = 0x8000;
7 | const GATEWAY_ID_MASK: u16 = 0x7fff;
8 |
9 | /// A gateway link layer address, which is either `To` or `From` a specific `GatewayID`.
10 | #[derive(Debug, Copy, Clone, Eq, PartialEq)]
11 | pub enum Address {
12 | /// `From` the indicated gateway, to the controller
13 | From(GatewayID),
14 | /// `To` the indicated gateway, from the controller
15 | To(GatewayID),
16 | }
17 |
18 | impl From for Address {
19 | fn from(value: u16) -> Self {
20 | let direction = value & DIRECTION_BIT;
21 | let id = GatewayID(value & GATEWAY_ID_MASK);
22 |
23 | if direction == 0 {
24 | Self::To(id)
25 | } else {
26 | Self::From(id)
27 | }
28 | }
29 | }
30 |
31 | impl From for u16 {
32 | fn from(value: Address) -> Self {
33 | match value {
34 | Address::From(id) => id.0 | DIRECTION_BIT,
35 | Address::To(id) => id.0,
36 | }
37 | }
38 | }
39 |
40 | impl From<[u8; 2]> for Address {
41 | fn from(value: [u8; 2]) -> Self {
42 | u16::from_be_bytes(value).into()
43 | }
44 | }
45 |
46 | impl From for [u8; 2] {
47 | fn from(value: Address) -> Self {
48 | u16::from(value).to_be_bytes()
49 | }
50 | }
51 |
52 | /// A 15-bit gateway ID.
53 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, JsonSchema)]
54 | #[serde(transparent)]
55 | pub struct GatewayID(u16);
56 |
57 | impl GatewayID {
58 | /// The all-zeroes gateway address
59 | pub const ZERO: GatewayID = GatewayID(0);
60 | }
61 |
62 | impl std::fmt::Debug for GatewayID {
63 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
64 | f.debug_tuple("GatewayID")
65 | .field(&format_args!("{:#04x}", self.0))
66 | .finish()
67 | }
68 | }
69 |
70 | impl std::fmt::Display for GatewayID {
71 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
72 | write!(f, "{:#04x}", self.0)
73 | }
74 | }
75 |
76 | #[derive(thiserror::Error, Debug, Copy, Clone, Eq, PartialEq)]
77 | #[error("invalid gateway ID {0:04x}")]
78 | pub struct InvalidGatewayID(u16);
79 |
80 | impl TryFrom for GatewayID {
81 | type Error = InvalidGatewayID;
82 |
83 | fn try_from(value: u16) -> Result {
84 | if value & GATEWAY_ID_MASK != value {
85 | Err(InvalidGatewayID(value))
86 | } else {
87 | Ok(GatewayID(value))
88 | }
89 | }
90 | }
91 |
92 | impl From for u16 {
93 | fn from(value: GatewayID) -> Self {
94 | value.0
95 | }
96 | }
97 |
98 | impl<'de> Deserialize<'de> for GatewayID {
99 | fn deserialize(deserializer: D) -> Result
100 | where
101 | D: Deserializer<'de>,
102 | {
103 | let id = u16::deserialize(deserializer)?;
104 | Self::try_from(id).map_err(D::Error::custom)
105 | }
106 | }
107 |
108 | #[cfg(test)]
109 | mod tests {
110 | use super::*;
111 |
112 | #[test]
113 | fn gateway_id() {
114 | assert_eq!(GatewayID::try_from(0), Ok(GatewayID(0)));
115 | assert_eq!(GatewayID::try_from(1), Ok(GatewayID(1)));
116 | assert_eq!(GatewayID::try_from(0x7fff), Ok(GatewayID(0x7fff)));
117 | assert_eq!(GatewayID::try_from(0x8000), Err(InvalidGatewayID(0x8000)));
118 | assert_eq!(GatewayID::try_from(0xffff), Err(InvalidGatewayID(0xffff)));
119 |
120 | assert_eq!(u16::from(GatewayID(1)), 1);
121 |
122 | assert_eq!(GatewayID(0x1201).to_string(), "0x1201");
123 | }
124 |
125 | #[test]
126 | fn address() {
127 | assert_eq!(Address::from([0x12, 0x01]), Address::To(GatewayID(0x1201)));
128 | assert_eq!(
129 | Address::from([0x92, 0x01]),
130 | Address::From(GatewayID(0x1201))
131 | );
132 |
133 | assert_eq!(
134 | [0x12, 0x01],
135 | <[u8; 2]>::from(Address::To(GatewayID(0x1201)))
136 | );
137 | assert_eq!(
138 | [0x92, 0x01],
139 | <[u8; 2]>::from(Address::From(GatewayID(0x1201)))
140 | );
141 | }
142 |
143 | #[test]
144 | fn address_fmt() {
145 | assert_eq!(format!("{:?}", &GatewayID(0x1201)), "GatewayID(0x1201)");
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/gateway/link/crc.rs:
--------------------------------------------------------------------------------
1 | // Standard CRC-16-CCITT 0x8048 polynomial CRC table
2 | const TABLE: [u16; 256] = [
3 | 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3,
4 | 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
5 | 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399,
6 | 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
7 | 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50,
8 | 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
9 | 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e,
10 | 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
11 | 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5,
12 | 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
13 | 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693,
14 | 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
15 | 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a,
16 | 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
17 | 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710,
18 | 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
19 | 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df,
20 | 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
21 | 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595,
22 | 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
23 | 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c,
24 | 0x3de3, 0x2c6a, 0x1ef1, 0x0f78,
25 | ];
26 |
27 | /// Calculate a CRC on a given buffer.
28 | pub fn crc(buffer: &[u8]) -> u16 {
29 | // This is a standard CRC-16-CCITT implementation, but with a nonstandard initial value of
30 | // 0x8408.
31 | //
32 | // (This is likely an implementation error upstream.)
33 | buffer.iter().fold(0x8408, |crc, b| {
34 | TABLE[(crc as u8 ^ b) as usize] ^ (crc >> 8)
35 | })
36 | }
37 |
38 | #[cfg(test)]
39 | mod tests {
40 | use super::*;
41 |
42 | #[test]
43 | fn test_vectors() {
44 | assert_eq!(crc(&[]), 0x8408);
45 | assert_eq!(crc(&[0x92]), 0x3B57);
46 | assert_eq!(crc(&[0x92, 0x01]), 0x3788);
47 | assert_eq!(
48 | crc(&[0x92, 0x01, 0x01, 0x49, 0x00, 0xFF, 0x7C, 0xDB, 0xC2]),
49 | 0x85A3
50 | );
51 |
52 | assert_eq!(
53 | crc(&[0x12, 0x01, 0x01, 0x48, 0x00, 0x01, 0x18, 0x82, 0x04]),
54 | 0x5DCF
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/gateway/link/escaping.rs:
--------------------------------------------------------------------------------
1 | //! Gateway link layer escaping.
2 |
3 | /// Determine the number of bytes needed to store the escaped version of a given input buffer.
4 | pub fn escaped_length(input: &[u8]) -> usize {
5 | input.len()
6 | + input
7 | .iter()
8 | .filter(|b| matches!(**b, 0x7e | 0x23..=0x25 | 0xa3..=0xa5))
9 | .count()
10 | }
11 |
12 | #[derive(thiserror::Error, Debug, Copy, Clone, Eq, PartialEq)]
13 | #[error("escaping error")]
14 | pub struct InvalidEscapeSequence;
15 |
16 | /// Apply link layer escaping.
17 | pub fn escape(buffer: &[u8], output: &mut Vec) {
18 | output.reserve(buffer.len());
19 |
20 | for byte in buffer {
21 | let escaped = match byte {
22 | 0x7e => 0x00,
23 | 0x24 => 0x01,
24 | 0x23 => 0x02,
25 | 0x25 => 0x03,
26 | 0xa4 => 0x04,
27 | 0xa3 => 0x05,
28 | 0xa5 => 0x06,
29 | _ => {
30 | output.push(*byte);
31 | continue;
32 | }
33 | };
34 | output.push(0x7e);
35 | output.push(escaped);
36 | }
37 | }
38 |
39 | pub fn unescaped_byte(byte_after_0x7e: u8) -> Result {
40 | match byte_after_0x7e {
41 | 0x00 => Ok(0x7e),
42 | 0x01 => Ok(0x24),
43 | 0x02 => Ok(0x23),
44 | 0x03 => Ok(0x25),
45 | 0x04 => Ok(0xa4),
46 | 0x05 => Ok(0xa3),
47 | 0x06 => Ok(0xa5),
48 | _ => Err(InvalidEscapeSequence),
49 | }
50 | }
51 |
52 | #[cfg(test)]
53 | mod tests {
54 | use super::*;
55 |
56 | const EXAMPLES: &[(&[u8], &[u8])] = &[
57 | (b"", b""),
58 | (b"~", b"\x7e\x00"),
59 | (b"hello", b"hello"),
60 | (b"~hello~", b"\x7e\x00hello\x7e\x00"),
61 | (
62 | b"\x7e\xa3\xa4\xa5\x23\x24\x25abcdef",
63 | b"\x7e\x00\x7e\x05\x7e\x04\x7e\x06\x7e\x02\x7e\x01\x7e\x03abcdef",
64 | ),
65 | (
66 | &[
67 | 0x92, 0x01, 0x01, 0x49, 0x00, 0xFF, 0x7C, 0xDB, 0xC2, 0xA3, 0x85,
68 | ],
69 | &[
70 | 0x92, 0x01, 0x01, 0x49, 0x00, 0xFF, 0x7C, 0xDB, 0xC2, 0x7E, 0x05, 0x85,
71 | ],
72 | ),
73 | ];
74 |
75 | #[test]
76 | fn test_escaped_length() {
77 | for (raw, escaped) in EXAMPLES.iter().copied() {
78 | assert_eq!(escaped_length(raw), escaped.len(), "{:?}", raw);
79 | }
80 | }
81 |
82 | #[test]
83 | fn test_escape() {
84 | for (raw, escaped) in EXAMPLES.iter().copied() {
85 | let mut output = Vec::new();
86 | escape(raw, &mut output);
87 | assert_eq!(output, escaped, "{:?}", raw);
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/gateway/link/receive.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | /// An object which handles reception callbacks.
4 | pub trait Sink {
5 | fn frame(&mut self, frame: Frame);
6 | }
7 |
8 | impl Sink for Vec {
9 | fn frame(&mut self, frame: Frame) {
10 | self.push(frame.clone());
11 | }
12 | }
13 |
14 | /// A receiver which converts a series of bytes into a series of `Frame`s.
15 | ///
16 | /// The receiver tolerates line errors and attempts to re-synchronize whenever possible. Errors are
17 | /// reported by incrementing counters.
18 | #[derive(Debug)]
19 | pub struct Receiver {
20 | sink: S,
21 | state: State,
22 | counters: Counters,
23 | buffer: Vec,
24 | }
25 |
26 | impl Receiver {
27 | const MAX_FRAME_SIZE: usize = 256;
28 |
29 | /// Instantiate a new receiver with a given `Sink`.
30 | pub fn new(sink: S) -> Self {
31 | Self {
32 | sink,
33 | state: Default::default(),
34 | counters: Default::default(),
35 | buffer: Default::default(),
36 | }
37 | }
38 |
39 | /// Access the `Sink`.
40 | pub fn sink(&self) -> &S {
41 | &self.sink
42 | }
43 |
44 | /// Mutably access the `Sink`.
45 | pub fn sink_mut(&mut self) -> &mut S {
46 | &mut self.sink
47 | }
48 |
49 | /// Destroy the `Receiver` to obtain the `Sink`.
50 | pub fn into_inner(self) -> S {
51 | self.sink
52 | }
53 |
54 | /// Retrieve the current counters describing the receiver's activity.
55 | pub fn counters(&self) -> &Counters {
56 | &self.counters
57 | }
58 |
59 | /// Reset the counters.
60 | pub fn reset_counters(&mut self) {
61 | self.counters = Counters::default();
62 | }
63 |
64 | /// Add a slice of bytes to the receiver.
65 | ///
66 | /// The receiver processes these bytes and calls functions on `Sink`.
67 | pub fn extend_from_slice(&mut self, buffer: &[u8]) {
68 | for byte in buffer {
69 | self.push_u8(*byte);
70 | }
71 | }
72 |
73 | /// Add a single byte to the receiver.
74 | fn push_u8(&mut self, byte: u8) {
75 | let next_state = match self.state {
76 | State::Idle => {
77 | match byte {
78 | // Preamble, expected
79 | 0x00 | 0xff => State::Idle,
80 | 0x7e => State::StartOfFrame,
81 | _ => State::Noise,
82 | }
83 | }
84 | State::Noise => {
85 | match byte {
86 | // Preamble, expected
87 | 0x00 | 0xff => State::Idle,
88 | // Possible start of frame
89 | 0x7e => State::StartOfFrame,
90 | // Discard
91 | _ => State::Noise,
92 | }
93 | }
94 | State::StartOfFrame => {
95 | match byte {
96 | // Proper start of frame
97 | 0x07 => State::Frame,
98 | // Improper
99 | _ => State::Noise,
100 | }
101 | }
102 | State::Frame => {
103 | match byte {
104 | // Escape sequence
105 | 0x7e => State::FrameEscape,
106 | // Normal data byte
107 | _ if self.buffer.len() < Self::MAX_FRAME_SIZE => {
108 | self.buffer.push(byte);
109 | State::Frame
110 | }
111 | // Overlong frame
112 | _ => State::Giant,
113 | }
114 | }
115 | State::FrameEscape => {
116 | if byte == 0x08 {
117 | // End of frame
118 | self.parse_frame_from_buffer();
119 | self.buffer.truncate(0);
120 | State::Idle
121 | } else if let Ok(byte) = escaping::unescaped_byte(byte) {
122 | if self.buffer.len() < Self::MAX_FRAME_SIZE {
123 | self.buffer.push(byte);
124 | State::Frame
125 | } else {
126 | self.buffer.truncate(0);
127 | State::GiantEscape
128 | }
129 | } else {
130 | self.buffer.truncate(0);
131 | State::Noise
132 | }
133 | }
134 | State::Giant => match byte {
135 | 0x7e => State::GiantEscape,
136 | _ => State::Giant,
137 | },
138 | State::GiantEscape => {
139 | match byte {
140 | // Start of frame
141 | 0x07 => State::Frame,
142 | // End of frame
143 | 0x08 => State::Idle,
144 | // Continue discarding
145 | _ => State::Giant,
146 | }
147 | }
148 | };
149 |
150 | match next_state {
151 | State::Noise if self.state != State::Noise => {
152 | self.counters.noise += 1;
153 | }
154 | State::Giant if self.state != State::Giant && self.state != State::GiantEscape => {
155 | self.buffer.truncate(0);
156 | self.counters.giants += 1;
157 | }
158 | _ => {}
159 | }
160 |
161 | self.state = next_state;
162 | }
163 |
164 | fn parse_frame_from_buffer(&mut self) {
165 | // Ensure we're a valid length
166 | if self.buffer.len() < 6 {
167 | self.counters.runts += 1;
168 | return;
169 | }
170 |
171 | // Verify the CRC
172 | let (body, expected_crc) = self.buffer.split_at(self.buffer.len() - 2);
173 | let crc = crc::crc(body);
174 | let expected_crc = u16::from_le_bytes([expected_crc[0], expected_crc[1]]);
175 | if expected_crc != crc {
176 | self.counters.checksums += 1;
177 | return;
178 | }
179 |
180 | let address = Address::from([body[0], body[1]]);
181 | let frame_type = Type(u16::from_be_bytes([body[2], body[3]]));
182 |
183 | self.counters.frames += 1;
184 | self.sink.frame(Frame {
185 | address,
186 | frame_type,
187 | payload: Vec::from(body.split_at(4).1),
188 | });
189 | }
190 | }
191 |
192 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
193 | enum State {
194 | #[default]
195 | Idle,
196 | Noise,
197 | StartOfFrame,
198 | Frame,
199 | FrameEscape,
200 | Giant,
201 | GiantEscape,
202 | }
203 |
204 | /// Counters describing the internal state transitions of a `Receiver`.
205 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
206 | pub struct Counters {
207 | /// The number of valid frames successfully received.
208 | pub frames: u64,
209 | /// The number of frames discarded for being too short.
210 | pub runts: u64,
211 | /// The number of frames discarded for being too long.
212 | pub giants: u64,
213 | /// The number of frames discarded for having an incorrect checksum.
214 | pub checksums: u64,
215 | /// The number of inter-frame periods where line noise was detected.
216 | pub noise: u64,
217 | }
218 |
219 | #[cfg(test)]
220 | mod tests {
221 | use super::*;
222 |
223 | #[test]
224 | fn happy_path() {
225 | let mut rx = Receiver::new(Vec::new());
226 | rx.extend_from_slice(&[
227 | 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x01, 0x48, 0x00, 0x01, 0x18, 0x83, 0x04,
228 | 0x17, 0x44, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x01, 0x49, 0x00, 0xFE, 0x01,
229 | 0x83, 0x5A, 0xDE, 0x07, 0x00, 0x0A, 0x01, 0x14, 0x63, 0x3A, /*…*/ 0x79, 0x26,
230 | 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x01, 0x48, 0x00, 0x01, 0x18,
231 | 0x84, 0x04, 0x1F, 0x09, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x01, 0x49, 0x00,
232 | 0xFF, 0x7C, 0xDB, 0xC2, 0x7E, 0x05, 0x85, 0x7E, 0x08,
233 | ]);
234 | assert_eq!(rx.state, State::Idle);
235 | assert_eq!(
236 | rx.counters,
237 | Counters {
238 | frames: 4,
239 | runts: 0,
240 | giants: 0,
241 | checksums: 0,
242 | noise: 0,
243 | }
244 | );
245 | assert_eq!(rx.buffer.len(), 0);
246 |
247 | assert_eq!(
248 | rx.sink,
249 | vec![
250 | Frame {
251 | address: Address::To(0x1201.try_into().unwrap()),
252 | frame_type: Type::RECEIVE_REQUEST,
253 | payload: b"\x00\x01\x18\x83\x04".as_slice().into(),
254 | },
255 | Frame {
256 | address: Address::From(0x1201.try_into().unwrap()),
257 | frame_type: Type::RECEIVE_RESPONSE,
258 | payload: b"\x00\xFE\x01\x83\x5A\xDE\x07\x00\x0A\x01\x14\x63\x3A"
259 | .as_slice()
260 | .into(),
261 | },
262 | Frame {
263 | address: Address::To(0x1201.try_into().unwrap()),
264 | frame_type: Type::RECEIVE_REQUEST,
265 | payload: b"\x00\x01\x18\x84\x04".as_slice().into(),
266 | },
267 | Frame {
268 | address: Address::From(0x1201.try_into().unwrap()),
269 | frame_type: Type::RECEIVE_RESPONSE,
270 | payload: b"\x00\xFF\x7C\xDB\xC2".as_slice().into(),
271 | },
272 | ]
273 | );
274 | }
275 |
276 | #[test]
277 | fn interframe_noise() {
278 | let mut rx = Receiver::new(Vec::new());
279 | rx.extend_from_slice(&[
280 | 0xee, 0xee, 0xee, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x01, 0x48, 0x00, 0x01,
281 | 0x18, 0x83, 0x04, 0x17, 0x44, 0x7E, 0x08, 0x01, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x01,
282 | 0x49, 0x00, 0xFE, 0x01, 0x83, 0x5A, 0xDE, 0x07, 0x00, 0x0A, 0x01, 0x14, 0x63, 0x3A,
283 | /*…*/ 0x79, 0x26, 0x7E, 0x08, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x00,
284 | 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x01, 0x48, 0x00, 0x01, 0x18, 0x84, 0x04, 0x1F,
285 | 0x09, 0x7E, 0x08, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xFF, 0x7E,
286 | 0x07, 0x92, 0x01, 0x01, 0x49, 0x00, 0xFF, 0x7C, 0xDB, 0xC2, 0x7E, 0x05, 0x85, 0x7E,
287 | 0x08,
288 | ]);
289 | assert_eq!(rx.state, State::Idle);
290 | assert_eq!(
291 | rx.counters,
292 | Counters {
293 | frames: 4,
294 | runts: 0,
295 | giants: 0,
296 | checksums: 0,
297 | noise: 3,
298 | }
299 | );
300 | assert_eq!(rx.buffer.len(), 0);
301 | }
302 |
303 | #[test]
304 | fn checksum() {
305 | let mut rx = Receiver::new(Vec::new());
306 | rx.extend_from_slice(&[
307 | 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x01, 0x48, 0x00, 0x01, 0x18, 0x83, 0x04,
308 | 0x17, 0x44, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x01, 0x49, 0x00, 0xFE, 0x01,
309 | 0x83, 0x5A, 0xDE, 0x07, 0x00, 0x0A, 0x01, 0x14, 0x63, 0x3A, /*…*/ 0x79, 0x25,
310 | 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x01, 0x48, 0x00, 0x01, 0x18,
311 | 0x84, 0x04, 0x1e, 0x09, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x01, 0x49, 0x00,
312 | 0xFF, 0x7C, 0xDB, 0xC2, 0x7E, 0x05, 0x85, 0x7E, 0x08,
313 | ]);
314 | assert_eq!(rx.state, State::Idle);
315 | assert_eq!(
316 | rx.counters,
317 | Counters {
318 | frames: 2,
319 | runts: 0,
320 | giants: 0,
321 | checksums: 2,
322 | noise: 0,
323 | }
324 | );
325 | assert_eq!(rx.buffer.len(), 0);
326 | }
327 |
328 | #[test]
329 | fn intraframe_noise() {
330 | let mut rx = Receiver::new(Vec::new());
331 | rx.extend_from_slice(&[
332 | 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x01, 0x48, 0x00, 0x01, 0x7e, 0x83, 0x04,
333 | 0x17, 0x44, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x01, 0x49, 0x00, 0xFE, 0x01,
334 | 0x83, 0x5A, 0xDE, 0x07, 0x00, 0x0A, 0x01, 0x14, 0x63, 0x3A, /*…*/ 0x79, 0x7e,
335 | 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x7e, 0x12, 0x01, 0x01, 0x48, 0x00, 0x01, 0x18,
336 | 0x84, 0x04, 0x1F, 0x09, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x01, 0x49, 0x00,
337 | 0xFF, 0x7C, 0xDB, 0xC2, 0x7E, 0x05, 0x85, 0x7E, 0x08,
338 | ]);
339 | assert_eq!(rx.state, State::Idle);
340 | assert_eq!(
341 | rx.counters,
342 | Counters {
343 | frames: 1,
344 | runts: 0,
345 | giants: 0,
346 | checksums: 0,
347 | noise: 6,
348 | }
349 | );
350 | assert_eq!(rx.buffer.len(), 0);
351 | }
352 |
353 | #[test]
354 | fn runt() {
355 | let mut rx = Receiver::new(Vec::new());
356 |
357 | let mut buf = Vec::new();
358 | buf.extend_from_slice(&[0xff, 0x7e, 0x07, 0x7e, 0x08]);
359 | buf.extend_from_slice(&[0x7e, 0x08]);
360 |
361 | rx.extend_from_slice(&[
362 | // underlength frames
363 | 0xFF, 0x7E, 0x07, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x00, 0x7E, 0x08, 0xFF, 0x7E, 0x07,
364 | 0x00, 0x00, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x00, 0x00, 0x00, 0x7E, 0x08, 0xFF, 0x7E,
365 | 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x08, // minimum length frame
366 | 0xFF, 0x7E, 0x07, 0x00, 0x01, 0x00, 0x00, 0x89, 0xD0, 0x7E, 0x08,
367 | ]);
368 | assert_eq!(rx.state, State::Idle);
369 | assert_eq!(
370 | rx.counters,
371 | Counters {
372 | frames: 1,
373 | runts: 5,
374 | giants: 0,
375 | checksums: 0,
376 | noise: 0,
377 | }
378 | );
379 | assert_eq!(rx.buffer.len(), 0);
380 | }
381 |
382 | #[test]
383 | fn giant() {
384 | let mut rx = Receiver::new(Vec::new());
385 | rx.extend_from_slice(&[0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01]);
386 | rx.extend_from_slice(&vec![0u8; 1000]);
387 | assert_eq!(rx.state, State::Giant);
388 | rx.extend_from_slice(&[0x7E]);
389 | assert_eq!(rx.state, State::GiantEscape);
390 | rx.extend_from_slice(&[0x08]);
391 | assert_eq!(rx.state, State::Idle);
392 | assert_eq!(
393 | rx.counters,
394 | Counters {
395 | frames: 0,
396 | runts: 0,
397 | giants: 1,
398 | checksums: 0,
399 | noise: 0,
400 | }
401 | );
402 | assert_eq!(rx.buffer.len(), 0);
403 | }
404 | }
405 |
--------------------------------------------------------------------------------
/src/gateway/physical.rs:
--------------------------------------------------------------------------------
1 | //! The gateway physical layer.
2 | //!
3 | //! This layer is responsible for RS-485 communication with gateway(s) like Tigo TAPs. This crate
4 | //! provides multiple implementations.
5 | //!
6 | //! * `serialport`, when compiled with the `serialport` feature
7 | //! * [`tcp`]
8 | //! * `termios`, when compiled on UNIX-like systems
9 |
10 | use std::fmt::Debug;
11 |
12 | pub trait Connection: std::io::Read + std::io::Write + Debug {}
13 |
14 | pub mod serialport;
15 |
16 | #[cfg(unix)]
17 | pub mod termios;
18 |
19 | pub mod tcp;
20 |
21 | //#[cfg(all(target_arch = "armv7l", target_os = "linux"))]
22 | //pub mod trace_meshdcd;
23 |
--------------------------------------------------------------------------------
/src/gateway/physical/serialport.rs:
--------------------------------------------------------------------------------
1 | use serialport::Result;
2 | use serialport::{
3 | available_ports, DataBits, FlowControl, Parity, SerialPort, SerialPortInfo, SerialPortType,
4 | StopBits,
5 | };
6 | use std::time::Duration;
7 |
8 | #[derive(Debug, Clone)]
9 | pub struct PortInfo(SerialPortInfo);
10 |
11 | impl PortInfo {
12 | pub fn list() -> Result> {
13 | available_ports().map(|vec| vec.into_iter().map(Self).collect())
14 | }
15 |
16 | pub fn open(&self) -> Result {
17 | Port::open(&self.0.port_name)
18 | }
19 |
20 | pub fn name(&self) -> &str {
21 | &self.0.port_name
22 | }
23 |
24 | pub fn port_type(&self) -> &SerialPortType {
25 | &self.0.port_type
26 | }
27 | }
28 |
29 | #[derive(Debug)]
30 | pub struct Port {
31 | pub inner: Box,
32 | }
33 |
34 | impl Port {
35 | pub fn open(name: &str) -> Result {
36 | serialport::new(name, 38400)
37 | .data_bits(DataBits::Eight)
38 | .parity(Parity::None)
39 | .stop_bits(StopBits::One)
40 | .flow_control(FlowControl::None)
41 | .timeout(Duration::from_millis(5))
42 | .open()
43 | .map(Port::new)
44 | }
45 |
46 | fn new(inner: Box) -> Self {
47 | Port { inner }
48 | }
49 | }
50 |
51 | impl std::io::Read for Port {
52 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
53 | loop {
54 | match self.inner.read(buf) {
55 | Ok(n) => return Ok(n),
56 | Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {
57 | continue;
58 | }
59 | Err(e) => return Err(e),
60 | }
61 | }
62 | }
63 | }
64 |
65 | impl std::io::Write for Port {
66 | fn write(&mut self, buf: &[u8]) -> std::io::Result {
67 | self.inner.write(buf)
68 | }
69 |
70 | fn flush(&mut self) -> std::io::Result<()> {
71 | self.inner.flush()
72 | }
73 | }
74 |
75 | impl super::Connection for Port {}
76 |
--------------------------------------------------------------------------------
/src/gateway/physical/tcp.rs:
--------------------------------------------------------------------------------
1 | use std::io::{Read, Write};
2 | use std::net::{TcpStream, ToSocketAddrs};
3 |
4 | /// A TCP serial connection.
5 | #[derive(Debug)]
6 | pub struct Connection {
7 | socket: TcpStream,
8 | readonly: bool,
9 | }
10 |
11 | impl Connection {
12 | pub fn connect(addr: A, readonly: bool) -> Result {
13 | let socket = TcpStream::connect(addr)?;
14 |
15 | Ok(Self { socket, readonly })
16 | }
17 | }
18 |
19 | impl super::Connection for Connection {}
20 |
21 | impl Read for Connection {
22 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
23 | self.socket.read(buf)
24 | }
25 | }
26 |
27 | impl Write for Connection {
28 | fn write(&mut self, buf: &[u8]) -> std::io::Result {
29 | if self.readonly {
30 | Err(std::io::ErrorKind::Unsupported.into())
31 | } else {
32 | self.socket.write(buf)
33 | }
34 | }
35 |
36 | fn flush(&mut self) -> std::io::Result<()> {
37 | if self.readonly {
38 | Ok(())
39 | } else {
40 | self.socket.flush()
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/gateway/physical/termios.rs:
--------------------------------------------------------------------------------
1 | use libc::{
2 | cfsetspeed, tcgetattr, tcsetattr, termios, B38400, CLOCAL, CREAD, CRTSCTS, CS8, CSIZE, CSTOPB,
3 | ECHO, ISIG, IXANY, IXOFF, IXON, OCRNL, ONLCR, OPOST, PARENB, TCSANOW, VMIN, VTIME,
4 | };
5 | use std::io::Error;
6 | use std::os::unix::io::AsRawFd;
7 | use std::path::Path;
8 |
9 | /// An open serial port.
10 | #[derive(Debug)]
11 | pub struct Port {
12 | file: std::fs::File,
13 | }
14 |
15 | impl Port {
16 | pub fn open>(device: P) -> Result {
17 | // Open the path which hopefully points to a serial port
18 | let file = std::fs::File::options()
19 | .read(true)
20 | .write(true)
21 | .open(device)?;
22 |
23 | unsafe {
24 | let fd = file.as_raw_fd();
25 | let mut tty: termios = std::mem::zeroed();
26 |
27 | // Get the terminal settings
28 | if tcgetattr(fd, &mut tty as *mut _) != 0 {
29 | return Err(Error::last_os_error());
30 | }
31 |
32 | // Use the helper ot set 38400 baud
33 | if cfsetspeed(&mut tty as *mut _, B38400) != 0 {
34 | return Err(Error::last_os_error());
35 | }
36 |
37 | // Now, in the structure directly, set:
38 | tty.c_cflag = (tty.c_cflag & !CSIZE) | CS8; // 8
39 | tty.c_cflag &= !PARENB; // N
40 | tty.c_cflag &= !CSTOPB; // 1
41 |
42 | tty.c_cflag &= !CRTSCTS; // no hardware flow control
43 | tty.c_iflag &= !(IXON | IXOFF | IXANY); // no software flow control
44 | tty.c_cflag |= CLOCAL; // disable modem status lines
45 | tty.c_cflag |= CREAD; // enable receiving
46 | tty.c_lflag &= !ECHO; // no local echo
47 | tty.c_lflag &= !ISIG; // don't interpret signal characters
48 | tty.c_oflag &= !OPOST; // don't post-process the output
49 | tty.c_oflag &= !(ONLCR | OCRNL); // specifically don't mangle CR/LF
50 |
51 | tty.c_cc[VMIN] = 1; // read at least 1 byte
52 | tty.c_cc[VTIME] = 0; // wait any amount of time for that byte
53 |
54 | // Update the FD
55 | if tcsetattr(fd, TCSANOW, &tty as *const _) != 0 {
56 | return Err(Error::last_os_error());
57 | }
58 | }
59 |
60 | Ok(Self { file })
61 | }
62 | }
63 |
64 | impl std::io::Read for Port {
65 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
66 | self.file.read(buf)
67 | }
68 | }
69 |
70 | impl std::io::Write for Port {
71 | fn write(&mut self, buf: &[u8]) -> std::io::Result {
72 | self.file.write(buf)
73 | }
74 |
75 | fn flush(&mut self) -> std::io::Result<()> {
76 | self.file.flush()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/gateway/physical/trace_meshdcd.rs:
--------------------------------------------------------------------------------
1 | //! A module to get input by using `ptrace` on `meshdcd`.
2 | //!
3 | //! `ptrace()` is a general purpose Linux process tracing mechanism. Some Tigo owners have `root`
4 | //! access on their controller devices, in which case they are free to use `ptrace()` to trace the
5 | //! `meshdcd` process which interfaces with the local serial port.
6 | //!
7 | //! This module inspects the `/proc` filesystem to identify `meshdcd` and the file descriptor of the
8 | //! local serial port. It then uses `ptrace()` to attach and intercept system calls. When `meshdcd`
9 | //! `read()` or `write()`s the serial port, this module reads the buffer containing the serial data.
10 |
11 | use std::error::Error;
12 |
13 | mod target;
14 | mod traced_process;
15 |
16 | use target::Target;
17 | use traced_process::TracedProcess;
18 |
19 | #[derive(thiserror::Error, Debug)]
20 | enum OpenError {
21 | #[error("error finding target process: {0}")]
22 | FindingTarget(#[from] target::FindTargetError),
23 | #[error("error attaching to `meshdcd` target: {0}")]
24 | Attaching(#[from] traced_process::AttachError),
25 | }
26 |
27 | pub fn open() -> Result, impl Error> {
28 | let target = Target::find().map_err(OpenError::from)?;
29 | let attached =
30 | TracedProcess::new(target.meshdcd_pid, target.meshdcd_tty_fd).map_err(OpenError::from)?;
31 | Ok(attached)
32 | }
33 |
--------------------------------------------------------------------------------
/src/gateway/physical/trace_meshdcd/target.rs:
--------------------------------------------------------------------------------
1 | use libc::{c_int, pid_t};
2 | use std::fs;
3 | use std::path::PathBuf;
4 |
5 | #[derive(Debug, Clone)]
6 | pub struct Target {
7 | pub(crate) meshdcd_pid: pid_t,
8 | pub(crate) meshdcd_tty_fd: c_int,
9 | }
10 |
11 | #[derive(thiserror::Error, Debug)]
12 | pub enum FindTargetError {
13 | #[error("reading /proc directory: {0}")]
14 | ReadProcDir(std::io::Error),
15 | #[error("reading /proc/{0}/exe: {1}")]
16 | ReadPidExe(pid_t, std::io::Error),
17 | #[error("reading /proc/{0}/fd: {1}")]
18 | ReadPidFds(pid_t, std::io::Error),
19 | #[error("no `meshdcd` process found")]
20 | NoMeshdcdProcess,
21 | #[error("`meshdcd` has no open tty")]
22 | MeshdcdHasNoTty,
23 | #[error("`meshdcd` has multiple open ttys")]
24 | MeshdcdHasMultipleTtys,
25 | }
26 |
27 | type Result = std::result::Result;
28 |
29 | fn read_proc_pids() -> Result>> {
30 | let readdir = fs::read_dir("/proc").map_err(FindTargetError::ReadProcDir)?;
31 | Ok(readdir.filter_map(|result| {
32 | result
33 | .map_err(FindTargetError::ReadProcDir)
34 | .map(|entry| {
35 | // See if we can parse this entry as a PID
36 | std::str::from_utf8(entry.file_name().as_encoded_bytes())
37 | .ok()
38 | .and_then(|filename| filename.parse::().ok())
39 | })
40 | .transpose()
41 | }))
42 | }
43 |
44 | fn is_meshdcd(pid: pid_t) -> bool {
45 | // Failures here might mean that the process exited while we're identifying it
46 | // Ignore them
47 | fs::read_link(format!("/proc/{}/exe", pid))
48 | .ok()
49 | .map(|path| {
50 | // Is this meshdcd?
51 | path.as_os_str().as_encoded_bytes().ends_with(b"/meshdcd")
52 | })
53 | .unwrap_or(false)
54 | }
55 |
56 | fn fds(pid: pid_t) -> Result> {
57 | let fd_path = PathBuf::from(format!("/proc/{}/fd", pid));
58 | fs::read_dir(&fd_path)
59 | .map_err(|e| FindTargetError::ReadPidFds(pid, e))?
60 | .map(|result| {
61 | let entry = result.map_err(|e| FindTargetError::ReadPidFds(pid, e))?;
62 |
63 | let fd: c_int = std::str::from_utf8(entry.file_name().as_encoded_bytes())
64 | .expect("fds must be UTF-8")
65 | .parse()
66 | .expect("fds must be ints");
67 |
68 | let entry_path = fd_path.clone().join(entry.file_name());
69 | let points_to =
70 | fs::read_link(&entry_path).map_err(|e| FindTargetError::ReadPidFds(pid, e))?;
71 | Ok((fd, points_to))
72 | })
73 | .collect()
74 | }
75 |
76 | impl Target {
77 | pub fn find() -> Result {
78 | // Find meshdcd
79 | let meshdcd_pid = read_proc_pids()?
80 | .filter_map(|pid| match pid {
81 | Ok(pid) if is_meshdcd(pid) => Some(Ok(pid)),
82 | Ok(_) => None,
83 | Err(e) => Some(Err(e)),
84 | })
85 | .next()
86 | .ok_or(FindTargetError::NoMeshdcdProcess)??;
87 |
88 | // Now that we have meshdcd, find which file descriptor points to a TTY
89 | let mut tty_fds = fds(meshdcd_pid)?
90 | .into_iter()
91 | .filter(|(fd, target)| {
92 | target
93 | .as_os_str()
94 | .as_encoded_bytes()
95 | .starts_with(b"/dev/tty")
96 | })
97 | .map(|(fd, _)| fd);
98 | let meshdcd_tty_fd = tty_fds.next().ok_or(FindTargetError::MeshdcdHasNoTty)?;
99 | if tty_fds.next().is_some() {
100 | return Err(FindTargetError::MeshdcdHasMultipleTtys);
101 | }
102 |
103 | Ok(Target {
104 | meshdcd_pid,
105 | meshdcd_tty_fd,
106 | })
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/gateway/physical/trace_meshdcd/traced_process.rs:
--------------------------------------------------------------------------------
1 | use crate::tap::{Event, Timestamp};
2 | use libc::{c_char, c_int, pid_t, waitpid, SIGTRAP, WIFSTOPPED, WSTOPSIG};
3 | use libc::{
4 | siginfo_t, user_regs, ESRCH, PTRACE_ATTACH, PTRACE_DETACH, PTRACE_GETREGS,
5 | PTRACE_O_TRACESYSGOOD, PTRACE_SETOPTIONS, PTRACE_SYSCALL, __WALL,
6 | };
7 | use std::os::unix::fs::FileExt;
8 | use std::ptr::null_mut;
9 |
10 | pub type Result = std::result::Result;
11 |
12 | #[derive(Debug)]
13 | pub struct TracedProcess {
14 | pid: pid_t,
15 | fd: c_int,
16 | mem: std::fs::File,
17 | }
18 |
19 | #[derive(thiserror::Error, Debug)]
20 | pub enum AttachError {
21 | #[error("PTRACE_ATTACH failed: {0}")]
22 | Ptrace(TraceError),
23 | #[error("error opening /proc/_/mem: {0}")]
24 | OpeningMem(std::io::Error),
25 | #[error("trace setup failed: {0}")]
26 | TraceSetup(TraceError),
27 | }
28 |
29 | impl TracedProcess {
30 | pub(crate) fn new(pid: pid_t, fd: c_int) -> Result {
31 | assert_ne!(pid, 0);
32 |
33 | // Attach the process
34 | unsafe { ptrace(PTRACE_ATTACH, pid, null_mut(), 0) }.map_err(AttachError::Ptrace)?;
35 |
36 | // Open memory
37 | let mem = std::fs::OpenOptions::new()
38 | .read(true)
39 | .write(true)
40 | .open(format!("/proc/{}/mem", pid));
41 | let mem = mem.map_err(|e| {
42 | // We're about to fail, but make sure we don't leave them hanging
43 | must_detach(pid);
44 | AttachError::OpeningMem(e)
45 | })?;
46 |
47 | // We can construct a TracedProcess, and we should since we want RAII
48 | let mut this = Self { pid, fd, mem };
49 |
50 | // Finish setup
51 | this.setup().map_err(AttachError::TraceSetup)?;
52 |
53 | Ok(this)
54 | }
55 |
56 | fn setup(&mut self) -> Result<()> {
57 | // Indicate that we want to identify system calls more easily
58 | unsafe {
59 | ptrace(
60 | PTRACE_SETOPTIONS,
61 | self.pid,
62 | null_mut(),
63 | PTRACE_O_TRACESYSGOOD,
64 | )?;
65 | }
66 |
67 | Ok(())
68 | }
69 |
70 | pub fn detach(mut self) {
71 | must_detach(self.pid);
72 | self.pid = 0;
73 | }
74 |
75 | fn wait_for_stop(&mut self) -> Result {
76 | // Wait for the tracee to stop
77 | let mut status: c_int = 0;
78 | loop {
79 | let pid = unsafe { waitpid(self.pid, &mut status as *mut c_int, __WALL) };
80 | if pid > 0 && WIFSTOPPED(status) {
81 | return Ok(status);
82 | } else if pid < 0 {
83 | return Err(TraceError::errno());
84 | }
85 | }
86 | }
87 |
88 | fn wait_for_syscall_stop(&mut self) -> Result<()> {
89 | loop {
90 | let status = self.wait_for_stop()?;
91 |
92 | // Tracee is stopped
93 | // Is it stopped for a system call?
94 | if WSTOPSIG(status) == SIGTRAP | 0x80 {
95 | return Ok(());
96 | }
97 |
98 | // It stopped for some other reason
99 | // Resume
100 | self.continue_until_syscall()?;
101 | }
102 | }
103 |
104 | fn continue_until_syscall(&mut self) -> Result<(), TraceError> {
105 | unsafe { ptrace(PTRACE_SYSCALL, self.pid, null_mut(), 0) }
106 | .map_err(|e| {
107 | eprintln!("PTRACE_SYSCALL error");
108 | e
109 | })
110 | .map(|_| ())
111 | }
112 |
113 | fn wait_for_next_event(&mut self) -> Result {
114 | loop {
115 | self.wait_for_syscall_stop()?;
116 |
117 | // We're stopped at a syscall
118 | // Get registers
119 | let regs = get_registers(self.pid)?;
120 |
121 | // Wait for the syscall to return
122 | self.continue_until_syscall()?;
123 | self.wait_for_stop()?;
124 |
125 | // Do we care about this syscall?
126 | // syscall # is r7, args are r0…r6
127 | let event = match regs.arm_r7 {
128 | SYSCALL_READ if regs.arm_r0 == self.fd as _ => {
129 | // We're reading the FD to trace
130 | // Wait for the system call to return
131 | self.generate_read_event(regs)?
132 | }
133 | SYSCALL_WRITE if regs.arm_r0 == self.fd as _ => {
134 | // We're writing the FD to trace
135 | // Wait for the system call to return
136 | self.generate_write_event(regs)?
137 | }
138 | _ => {
139 | // Nah
140 | None
141 | }
142 | };
143 | self.continue_until_syscall()?;
144 |
145 | if let Some(e) = event {
146 | return Ok(e);
147 | }
148 |
149 | // Go around again
150 | }
151 | }
152 |
153 | fn generate_read_event(&self, call_regs: user_regs) -> Result> {
154 | let now = Timestamp::now();
155 |
156 | let return_regs = get_registers(self.pid)?;
157 | let buffer_ptr = call_regs.arm_r1;
158 |
159 | let bytes_read = return_regs.arm_r0 as isize;
160 | if bytes_read < 0 {
161 | // made no progress
162 | return Ok(None);
163 | }
164 |
165 | let mut buffer = vec![0u8; bytes_read as usize];
166 | self.mem
167 | .read_at(&mut buffer, buffer_ptr as _)
168 | .map_err(TraceError::MemoryReadError)?;
169 | Ok(Some(Event::SerialRx(now, buffer)))
170 | }
171 |
172 | fn generate_write_event(&self, call_regs: user_regs) -> Result > {
173 | let now = Timestamp::now();
174 |
175 | let return_regs = get_registers(self.pid)?;
176 | let buffer_ptr = call_regs.arm_r1;
177 |
178 | let bytes = return_regs.arm_r0 as isize;
179 | if bytes < 0 {
180 | // made no progress
181 | return Ok(None);
182 | }
183 |
184 | let mut buffer = vec![0u8; bytes as usize];
185 | self.mem
186 | .read_at(&mut buffer, buffer_ptr as _)
187 | .map_err(TraceError::MemoryReadError)?;
188 | Ok(Some(Event::SerialTx(now, buffer)))
189 | }
190 | }
191 |
192 | impl Iterator for TracedProcess {
193 | type Item = super::Event;
194 |
195 | fn next(&mut self) -> Option {
196 | match self.wait_for_next_event() {
197 | Ok(e) => Some(e),
198 | Err(e) => return Some(Event::Error(e)),
199 | }
200 | }
201 | }
202 |
203 | const SYSCALL_READ: u32 = 3;
204 | const SYSCALL_WRITE: u32 = 4;
205 |
206 | impl Drop for TracedProcess {
207 | fn drop(&mut self) {
208 | if self.pid != 0 {
209 | must_detach(self.pid);
210 | }
211 | }
212 | }
213 |
214 | #[derive(thiserror::Error, Debug)]
215 | pub enum TraceError {
216 | #[error("process terminated")]
217 | ProcessTerminated,
218 | #[error("ptrace error: {0}")]
219 | General(std::io::Error),
220 | #[error("memory read failed: {0}")]
221 | MemoryReadError(std::io::Error),
222 | }
223 |
224 | impl TraceError {
225 | fn errno() -> Self {
226 | let e = std::io::Error::last_os_error();
227 | match e.raw_os_error() {
228 | Some(ESRCH) => TraceError::ProcessTerminated,
229 | _ => TraceError::General(e),
230 | }
231 | }
232 | }
233 |
234 | unsafe fn ptrace(
235 | request: libc::c_uint,
236 | pid: pid_t,
237 | addr: *mut libc::c_char,
238 | data: libc::c_int,
239 | ) -> Result {
240 | let rv = libc::ptrace(request as _, pid, addr, data);
241 | if rv == -1 {
242 | Err(TraceError::errno())
243 | } else {
244 | Ok(rv)
245 | }
246 | }
247 |
248 | fn must_detach(pid: pid_t) {
249 | unsafe { ptrace(PTRACE_DETACH, pid, null_mut(), 0) }.expect("PTRACE_DETACH");
250 | }
251 |
252 | fn get_registers(pid: pid_t) -> Result {
253 | let mut regs: user_regs = unsafe { std::mem::zeroed() };
254 | unsafe { ptrace(PTRACE_GETREGS, pid, null_mut(), &mut regs as *mut _ as _) }.map_err(|e| {
255 | eprintln!("PTRACE_GETREGS error");
256 | e
257 | })?;
258 | Ok(regs)
259 | }
260 |
--------------------------------------------------------------------------------
/src/gateway/transport.rs:
--------------------------------------------------------------------------------
1 | use schemars::JsonSchema;
2 | use serde::{Deserialize, Serialize};
3 | // Use `zerocopy` to transmute `#[repr(C)]` structs to/from byte slices
4 | use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
5 |
6 | // Everything at this layer is big endian
7 | use pv::application::PacketType;
8 | use zerocopy::byteorder::big_endian::U16;
9 |
10 | mod receiver;
11 | use crate::gateway::link::{Address, GatewayID};
12 | use crate::pv;
13 | use crate::pv::link::SlotCounter;
14 | pub use receiver::{Counters, Receiver, Sink};
15 |
16 | #[derive(
17 | Debug,
18 | Copy,
19 | Clone,
20 | Eq,
21 | PartialEq,
22 | Ord,
23 | PartialOrd,
24 | FromBytes,
25 | IntoBytes,
26 | Unaligned,
27 | KnownLayout,
28 | Immutable,
29 | Serialize,
30 | Deserialize,
31 | JsonSchema,
32 | )]
33 | #[repr(transparent)]
34 | #[serde(transparent)]
35 | pub struct CommandSequenceNumber(pub u8);
36 |
37 | /// A command request frame payload.
38 | #[derive(
39 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable,
40 | )]
41 | #[repr(C)]
42 | pub struct CommandRequest {
43 | pub unknown: [u8; 3],
44 | pub packet_type: PacketType,
45 | pub sequence_number: CommandSequenceNumber,
46 | }
47 |
48 | /// A command response frame payload.
49 | #[derive(
50 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable,
51 | )]
52 | #[repr(C)]
53 | pub struct CommandResponse {
54 | pub unknown_1: u8,
55 | pub tx_buffers_free: u8,
56 | pub unknown_2: u8,
57 | pub packet_type: PacketType,
58 | pub command_sequence_number: CommandSequenceNumber,
59 | }
60 |
61 | /// A receive request frame payload.
62 | #[derive(
63 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable,
64 | )]
65 | #[repr(C)]
66 | pub struct ReceiveRequest {
67 | pub unknown_1: [u8; 2],
68 | pub packet_number: U16,
69 | pub unknown_2: u8,
70 | }
71 |
72 | /// A receive response frame payload, decoded into its most general form.
73 | #[derive(Debug, Copy, Clone, Eq, PartialEq)]
74 | pub struct ReceiveResponse {
75 | pub rx_buffers_used: Option,
76 | pub tx_buffers_free: Option,
77 | pub unknown_a: Option<[u8; 2]>,
78 | pub unknown_b: Option<[u8; 2]>,
79 | pub packet_number: u16,
80 | pub slot_counter: SlotCounter,
81 | }
82 |
83 | fn interpret_packet_number_lo(new_lo: u8, old: u16) -> u16 {
84 | let [old_hi, old_lo] = old.to_be_bytes();
85 | let new_hi = if new_lo >= old_lo {
86 | old_hi
87 | } else {
88 | // wrap
89 | old_hi + 1
90 | };
91 | u16::from_be_bytes([new_hi, new_lo])
92 | }
93 |
94 | #[derive(thiserror::Error, Debug, Copy, Clone, Eq, PartialEq)]
95 | pub enum InvalidReceiveResponse {
96 | #[error("too short: expected at least {0} bytes")]
97 | TooShort(usize),
98 | #[error("invalid status type: {0:#06x}")]
99 | UnknownStatusType(u16),
100 | }
101 |
102 | impl ReceiveResponse {
103 | /// Attempt to interpret a byte slice as a `ReceiveResponse`, using an existing packet number
104 | /// for reference.
105 | pub fn read_from_bytes(
106 | bytes: &[u8],
107 | packet_number: u16,
108 | ) -> Result<(Self, pv::network::ReceivedPackets), InvalidReceiveResponse> {
109 | // Ensure we have at least a minimal length
110 | if bytes.len() < 2 {
111 | return Err(InvalidReceiveResponse::TooShort(5));
112 | };
113 |
114 | // Read the status type bitmask
115 | let status_type = U16::ref_from_bytes(&bytes[0..2]).unwrap().get();
116 |
117 | // Ensure it matches the known patterns
118 | if status_type & 0xffe0 != 0x00e0 {
119 | return Err(InvalidReceiveResponse::UnknownStatusType(status_type));
120 | }
121 |
122 | // Split off the rest
123 | let (_, mut rest) = bytes.split_at(2);
124 |
125 | let expected_length = if status_type & 0x0001 == 0 { 1 } else { 0 }
126 | + if status_type & 0x0002 == 0 { 1 } else { 0 }
127 | + if status_type & 0x0004 == 0 { 2 } else { 0 }
128 | + if status_type & 0x0008 == 0 { 2 } else { 0 }
129 | + if status_type & 0x0010 == 0 { 2 } else { 1 }
130 | + 2;
131 | if rest.len() < expected_length {
132 | return Err(InvalidReceiveResponse::TooShort(expected_length + 2));
133 | }
134 |
135 | // Grab rx_buffers_used, if any
136 | let rx_buffers_used = if status_type & 0x0001 == 0 {
137 | let (value, new_rest) = rest.split_at(1);
138 | rest = new_rest;
139 | Some(value[0])
140 | } else {
141 | None
142 | };
143 |
144 | // Grab tx_buffers_free, if any
145 | let tx_buffers_free = if status_type & 0x0002 == 0 {
146 | let (value, new_rest) = rest.split_at(1);
147 | rest = new_rest;
148 | Some(value[0])
149 | } else {
150 | None
151 | };
152 |
153 | // Grab unknown_a, if any
154 | let unknown_a = if status_type & 0x0004 == 0 {
155 | let (value, new_rest) = rest.split_at(2);
156 | rest = new_rest;
157 | Some([value[0], value[1]])
158 | } else {
159 | None
160 | };
161 |
162 | // Grab unknown_b, if any
163 | let unknown_b = if status_type & 0x0008 == 0 {
164 | let (value, new_rest) = rest.split_at(2);
165 | rest = new_rest;
166 | Some([value[0], value[1]])
167 | } else {
168 | None
169 | };
170 |
171 | // Grab packet number, expanding as needed
172 | let packet_number = if status_type & 0x0010 == 0 {
173 | let (value, new_rest) = rest.split_at(2);
174 | rest = new_rest;
175 | u16::from_be_bytes([value[0], value[1]])
176 | } else {
177 | let (value, new_rest) = rest.split_at(1);
178 | rest = new_rest;
179 | interpret_packet_number_lo(value[0], packet_number)
180 | };
181 |
182 | // Grab slot counter
183 | let (slot_counter, new_rest) = rest.split_at(2);
184 | rest = new_rest;
185 | let slot_counter = SlotCounter::read_from_bytes(slot_counter).unwrap();
186 |
187 | Ok((
188 | Self {
189 | rx_buffers_used,
190 | tx_buffers_free,
191 | unknown_a,
192 | unknown_b,
193 | packet_number,
194 | slot_counter,
195 | },
196 | pv::network::ReceivedPackets(rest),
197 | ))
198 | }
199 | }
200 |
201 | /// An identify response frame payload.
202 | #[derive(
203 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable,
204 | )]
205 | #[repr(C)]
206 | pub struct IdentifyResponse {
207 | pub pv_long_address: pv::LongAddress,
208 | pub gateway_address: [u8; 2],
209 | }
210 |
211 | impl IdentifyResponse {
212 | pub fn gateway_id(&self) -> Option {
213 | match Address::from(self.gateway_address) {
214 | Address::From(_) => None,
215 | Address::To(id) => Some(id),
216 | }
217 | }
218 | }
219 |
220 | /// An enumeration start request frame payload.
221 | #[derive(
222 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable,
223 | )]
224 | #[repr(C)]
225 | pub struct EnumerationStartRequest {
226 | pub unknown: [u8; 4],
227 | pub enumeration_address: [u8; 2],
228 | }
229 |
230 | impl EnumerationStartRequest {
231 | pub fn enumeration_gateway_id(&self) -> Option {
232 | match Address::from(self.enumeration_address) {
233 | Address::From(_) => None,
234 | Address::To(id) => Some(id),
235 | }
236 | }
237 | }
238 |
239 | #[cfg(test)]
240 | mod tests {
241 | use super::*;
242 | use crate::pv::network::ReceivedPackets;
243 |
244 | #[test]
245 | fn rx_request_from_bytes() {
246 | assert_eq!(
247 | ReceiveRequest::read_from_bytes(&[0x00, 0x01, 0x18, 0x83, 0x04]),
248 | Ok(ReceiveRequest {
249 | unknown_1: [0x00, 0x01],
250 | packet_number: 0x1883.into(),
251 | unknown_2: 0x04,
252 | })
253 | );
254 | }
255 |
256 | #[test]
257 | fn rx_response_from_bytes() {
258 | assert_eq!(
259 | ReceiveResponse::read_from_bytes(
260 | &[0x00, 0xE0, 0x04, 0x0E, 0x00, 0x01, 0x02, 0x00, 0x40, 0xFB, 0x21, 0x1B, 1, 2, 3],
261 | 0x40FB,
262 | ),
263 | Ok((
264 | ReceiveResponse {
265 | rx_buffers_used: Some(0x04),
266 | tx_buffers_free: Some(0x0E),
267 | unknown_a: Some([0x00, 0x01]),
268 | unknown_b: Some([0x02, 0x00]),
269 | packet_number: 0x40FB,
270 | slot_counter: 0x211B.into(),
271 | },
272 | ReceivedPackets(&[1, 2, 3])
273 | ))
274 | );
275 |
276 | assert_eq!(
277 | ReceiveResponse::read_from_bytes(&[0x00, 0xFE, 0x02, 0xFF, 0x21, 0x22, 4], 0x40FB),
278 | Ok((
279 | ReceiveResponse {
280 | rx_buffers_used: Some(0x02),
281 | tx_buffers_free: None,
282 | unknown_a: None,
283 | unknown_b: None,
284 | packet_number: 0x40FF,
285 | slot_counter: 0x2122.into(),
286 | },
287 | ReceivedPackets(&[4])
288 | ))
289 | );
290 |
291 | assert_eq!(
292 | ReceiveResponse::read_from_bytes(&[0x00, 0xEE, 0x00, 0x41, 0x01, 0x21, 0x27], 0x40FB),
293 | Ok((
294 | ReceiveResponse {
295 | rx_buffers_used: Some(0x00),
296 | tx_buffers_free: None,
297 | unknown_a: None,
298 | unknown_b: None,
299 | packet_number: 0x4101,
300 | slot_counter: 0x2127.into(),
301 | },
302 | ReceivedPackets(&[])
303 | ))
304 | );
305 |
306 | assert_eq!(
307 | ReceiveResponse::read_from_bytes(&[0x00, 0xEE, 0x00, 0x41, 0x01, 0x21], 0x40FB),
308 | Err(InvalidReceiveResponse::TooShort(7))
309 | );
310 | assert_eq!(
311 | ReceiveResponse::read_from_bytes(&[0x00, 0xEE, 0x00, 0x41, 0x01], 0x40FB),
312 | Err(InvalidReceiveResponse::TooShort(7))
313 | );
314 | assert_eq!(
315 | ReceiveResponse::read_from_bytes(&[0x00, 0xEE, 0x00, 0x41], 0x40FB),
316 | Err(InvalidReceiveResponse::TooShort(7))
317 | );
318 | assert_eq!(
319 | ReceiveResponse::read_from_bytes(&[0x00, 0xEE, 0x00], 0x40FB),
320 | Err(InvalidReceiveResponse::TooShort(7))
321 | );
322 | assert_eq!(
323 | ReceiveResponse::read_from_bytes(&[0x00, 0xEE], 0x40FB),
324 | Err(InvalidReceiveResponse::TooShort(7))
325 | );
326 |
327 | assert_eq!(
328 | ReceiveResponse::read_from_bytes(&[0x00, 0xFF, 0x03, 0x21, 0x31], 0x40FB),
329 | Ok((
330 | ReceiveResponse {
331 | rx_buffers_used: None,
332 | tx_buffers_free: None,
333 | unknown_a: None,
334 | unknown_b: None,
335 | packet_number: 0x4103,
336 | slot_counter: 0x2131.into(),
337 | },
338 | ReceivedPackets(&[])
339 | ))
340 | );
341 |
342 | assert_eq!(
343 | ReceiveResponse::read_from_bytes(&[0x00, 0xFF, 0x03, 0x21], 0x40FB),
344 | Err(InvalidReceiveResponse::TooShort(5))
345 | );
346 | assert_eq!(
347 | ReceiveResponse::read_from_bytes(&[0x00, 0xFF, 0x03], 0x40FB),
348 | Err(InvalidReceiveResponse::TooShort(5))
349 | );
350 |
351 | assert_eq!(
352 | ReceiveResponse::read_from_bytes(&[0x00, 0xFF], 0x40FB),
353 | Err(InvalidReceiveResponse::TooShort(5))
354 | );
355 | }
356 |
357 | #[test]
358 | fn identify_response_payload() {
359 | let expected = IdentifyResponse {
360 | pv_long_address: pv::LongAddress([0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16]),
361 | gateway_address: [0x12, 0x01],
362 | };
363 | assert_eq!(
364 | IdentifyResponse::read_from_bytes(&[
365 | 0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16, 0x12, 0x01
366 | ]),
367 | Ok(expected)
368 | );
369 | assert_eq!(expected.gateway_id(), Some(0x1201.try_into().unwrap()));
370 | }
371 | }
372 |
--------------------------------------------------------------------------------
/src/gateway/transport/receiver.rs:
--------------------------------------------------------------------------------
1 | use super::super::link::{self, Frame, GatewayID};
2 | use super::*;
3 | use crate::gateway::link::Address;
4 | use crate::pv;
5 | use crate::pv::link::SlotCounter;
6 | use crate::pv::network::ReceivedPacketHeader;
7 | use std::collections::btree_map::Entry;
8 | use std::collections::BTreeMap;
9 | use std::mem::size_of;
10 |
11 | pub trait Sink {
12 | /// Enumeration started, using the indicated gateway ID.
13 | fn enumeration_started(&mut self, enumeration_gateway_id: GatewayID);
14 |
15 | /// A gateway's address was observed.
16 | ///
17 | /// If the network is enumerating, the gateway ID may be the `enumeration_gateway_id`, in which
18 | /// case this ID may not be unique.
19 | fn gateway_identity_observed(&mut self, gateway_id: GatewayID, address: pv::LongAddress);
20 |
21 | /// A gateway's version was observed.
22 | fn gateway_version_observed(&mut self, gateway_id: GatewayID, version: &str);
23 |
24 | /// Enumeration ended.
25 | fn enumeration_ended(&mut self, gateway_id: GatewayID);
26 |
27 | /// A gateway's slot counter was captured inside the gateway.
28 | ///
29 | /// The value of the slot counter at this moment may be described by a subsequent call to
30 | /// `gateway_slot_counter_observed()`.
31 | fn gateway_slot_counter_captured(&mut self, gateway_id: GatewayID);
32 |
33 | /// A gateway's slot counter was observed.
34 | ///
35 | /// The indicated slot counter value corresponds to the moment when the counter was most
36 | /// recently captured by the gateway, which occurred 4 to 50+ milliseconds ago.
37 | fn gateway_slot_counter_observed(&mut self, gateway_id: GatewayID, slot_counter: SlotCounter);
38 |
39 | /// A PV network packet was received from a gateway.
40 | fn packet_received(
41 | &mut self,
42 | gateway_id: GatewayID,
43 | header: &ReceivedPacketHeader,
44 | data: &[u8],
45 | );
46 |
47 | /// A command was executed by a gateway.
48 | fn command_executed(
49 | &mut self,
50 | gateway_id: GatewayID,
51 | request: (PacketType, &[u8]),
52 | response: (PacketType, &[u8]),
53 | );
54 | }
55 |
56 | #[derive(Debug, Clone)]
57 | pub struct Receiver {
58 | sink: S,
59 | rx_packet_numbers: BTreeMap,
60 | command_sequence_numbers: BTreeMap,
61 | commands_awaiting_response: BTreeMap<(GatewayID, CommandSequenceNumber), (PacketType, Vec)>,
62 | counters: Counters,
63 | }
64 |
65 | impl link::Sink for Receiver {
66 | fn frame(&mut self, frame: Frame) {
67 | match frame.frame_type {
68 | link::Type::RECEIVE_REQUEST => {
69 | self.receive_request(frame);
70 | }
71 | link::Type::RECEIVE_RESPONSE => {
72 | self.receive_response(frame);
73 | }
74 | link::Type::COMMAND_REQUEST => {
75 | self.command_request(frame);
76 | }
77 | link::Type::COMMAND_RESPONSE => {
78 | self.command_response(frame);
79 | }
80 | link::Type::PING_REQUEST => {
81 | self.counters.ping_requests += 1;
82 | }
83 | link::Type::PING_RESPONSE => {
84 | self.counters.ping_responses += 1;
85 | }
86 | link::Type::ENUMERATION_START_REQUEST => {
87 | self.enumeration_start_request(frame);
88 | }
89 | link::Type::ENUMERATION_START_RESPONSE => {
90 | self.counters.enumeration_start_responses += 1;
91 | }
92 | link::Type::ENUMERATION_REQUEST => {
93 | self.counters.enumeration_requests += 1;
94 | }
95 | link::Type::ENUMERATION_RESPONSE => {
96 | self.enumeration_response(frame);
97 | }
98 | link::Type::ASSIGN_GATEWAY_ID_REQUEST => {
99 | self.counters.assign_gateway_id_requests += 1;
100 | }
101 | link::Type::ASSIGN_GATEWAY_ID_RESPONSE => {
102 | self.counters.assign_gateway_id_responses += 1;
103 | }
104 | link::Type::IDENTIFY_REQUEST => {
105 | self.counters.identify_requests += 1;
106 | }
107 | link::Type::IDENTIFY_RESPONSE => {
108 | self.identify_response(frame);
109 | }
110 | link::Type::VERSION_REQUEST => {
111 | self.counters.version_requests += 1;
112 | }
113 | link::Type::VERSION_RESPONSE => {
114 | self.version_response(frame);
115 | }
116 | link::Type::ENUMERATION_END_REQUEST => {
117 | self.counters.enumeration_end_requests += 1;
118 | }
119 | link::Type::ENUMERATION_END_RESPONSE => match frame.address {
120 | Address::From(gateway) => {
121 | self.counters.enumeration_end_responses += 1;
122 | self.sink.enumeration_ended(gateway);
123 | }
124 | Address::To(_) => {
125 | self.counters.invalid_enumeration_end_responses += 1;
126 | }
127 | },
128 | _ => {
129 | self.counters.unhandled_frame_type += 1;
130 | }
131 | }
132 | }
133 | }
134 |
135 | impl Receiver {
136 | /// Instantiate a new receiver with a given `Sink`.
137 | pub fn new(sink: S) -> Self {
138 | Self {
139 | sink,
140 | rx_packet_numbers: Default::default(),
141 | command_sequence_numbers: Default::default(),
142 | commands_awaiting_response: Default::default(),
143 | counters: Default::default(),
144 | }
145 | }
146 |
147 | /// Access the `Sink`.
148 | pub fn sink(&self) -> &S {
149 | &self.sink
150 | }
151 |
152 | /// Mutably access the `Sink`.
153 | pub fn sink_mut(&mut self) -> &mut S {
154 | &mut self.sink
155 | }
156 |
157 | /// Destroy the `Receiver` to obtain the `Sink`.
158 | pub fn into_inner(self) -> S {
159 | self.sink
160 | }
161 |
162 | /// Retrieve the current counters describing the receiver's activity.
163 | pub fn counters(&self) -> &Counters {
164 | &self.counters
165 | }
166 |
167 | /// Reset the counters.
168 | pub fn reset_counters(&mut self) {
169 | self.counters = Default::default();
170 | }
171 |
172 | fn receive_request(&mut self, frame: Frame) {
173 | let Address::To(gateway_id) = frame.address else {
174 | self.counters.invalid_receive_request += 1;
175 | return;
176 | };
177 |
178 | let Ok(payload) = ReceiveRequest::ref_from_bytes(frame.payload.as_ref()) else {
179 | self.counters.invalid_receive_request += 1;
180 | return;
181 | };
182 |
183 | // Indicate that the gateway captured its slot counter now, while processing the receive
184 | // request
185 | self.sink.gateway_slot_counter_captured(gateway_id);
186 |
187 | self.counters.receive_requests += 1;
188 |
189 | // Record the packet number for this gateway
190 | let n: u16 = payload.packet_number.into();
191 | *self.rx_packet_numbers.entry(gateway_id).or_insert(n) = n;
192 | }
193 |
194 | fn receive_response(&mut self, frame: Frame) {
195 | let Address::From(gateway_id) = frame.address else {
196 | self.counters.invalid_receive_responses += 1;
197 | return;
198 | };
199 |
200 | // Get the packet number for this gateway
201 | let Some(n) = self.rx_packet_numbers.get_mut(&gateway_id) else {
202 | self.counters.receive_response_from_unknown_gateway += 1;
203 | return;
204 | };
205 |
206 | // Interpret the response
207 | let Ok((status, packets)) = ReceiveResponse::read_from_bytes(frame.payload.as_ref(), *n)
208 | else {
209 | self.counters.invalid_receive_responses += 1;
210 | return;
211 | };
212 |
213 | self.counters.receive_responses += 1;
214 |
215 | // TODO: deduplicate gateway -> controller retransmissions
216 |
217 | // Update the packet number
218 | *n = status.packet_number;
219 |
220 | // Observe the slot counter
221 | self.sink
222 | .gateway_slot_counter_observed(gateway_id, status.slot_counter);
223 |
224 | for packet in packets {
225 | if let Ok((header, data)) = packet {
226 | self.counters.receive_packets += 1;
227 |
228 | // Observe the packet
229 | self.sink.packet_received(gateway_id, header, data);
230 | } else {
231 | self.counters.receive_packet_too_short += 1;
232 | }
233 | }
234 | }
235 |
236 | fn command_request(&mut self, frame: Frame) {
237 | let Address::To(gateway_id) = frame.address else {
238 | println!("bad tx request: {:?}", frame);
239 | self.counters.invalid_command_requests += 1;
240 | return;
241 | };
242 |
243 | if frame.payload.len() < size_of::() {
244 | println!("bad tx request: {:?}", frame);
245 | self.counters.invalid_command_requests += 1;
246 | return;
247 | }
248 |
249 | let (header, payload) = frame.payload.split_at(size_of::());
250 | let header = CommandRequest::ref_from_bytes(header).unwrap(); // infallible
251 |
252 | // The gateway may respond to this, so record it
253 | self.commands_awaiting_response.insert(
254 | (gateway_id, header.sequence_number),
255 | (header.packet_type, payload.to_vec()),
256 | );
257 |
258 | // Is this a retransmission from our vantage point?
259 | let retransmission = match self.command_sequence_numbers.entry(gateway_id) {
260 | Entry::Occupied(e) if *e.get() == header.sequence_number => true,
261 | Entry::Occupied(mut e) => {
262 | e.insert(header.sequence_number);
263 | false
264 | }
265 | Entry::Vacant(e) => {
266 | e.insert(header.sequence_number);
267 | false
268 | }
269 | };
270 |
271 | // Count it appropriately
272 | if retransmission {
273 | self.counters.retransmitted_command_requests += 1;
274 | } else {
275 | self.counters.command_requests += 1;
276 | }
277 | }
278 |
279 | fn command_response(&mut self, frame: Frame) {
280 | let Address::From(gateway_id) = frame.address else {
281 | println!("wrong addr: {:?}", frame);
282 | self.counters.invalid_command_responses += 1;
283 | return;
284 | };
285 |
286 | if frame.payload.len() < size_of::() {
287 | println!("bad tx response: {:?}", frame);
288 | self.counters.invalid_command_responses += 1;
289 | return;
290 | };
291 |
292 | let (header, payload) = frame.payload.split_at(size_of::());
293 | let header = CommandResponse::ref_from_bytes(header).unwrap(); // infallible
294 |
295 | // Deduplicate responses
296 | let Some((request_packet_type, request_payload)) = self
297 | .commands_awaiting_response
298 | .remove(&(gateway_id, header.command_sequence_number))
299 | else {
300 | self.counters.retransmitted_command_responses += 1;
301 | return;
302 | };
303 |
304 | self.counters.command_responses += 1;
305 |
306 | self.sink.command_executed(
307 | gateway_id,
308 | (request_packet_type, request_payload.as_slice()),
309 | (header.packet_type, payload),
310 | );
311 | }
312 |
313 | fn enumeration_start_request(&mut self, frame: Frame) {
314 | let Address::To(GatewayID::ZERO) = frame.address else {
315 | self.counters.invalid_enumeration_start_request += 1;
316 | return;
317 | };
318 |
319 | let Ok(request) = EnumerationStartRequest::ref_from_bytes(frame.payload.as_ref()) else {
320 | self.counters.invalid_enumeration_start_request += 1;
321 | return;
322 | };
323 |
324 | let Some(gateway_id) = request.enumeration_gateway_id() else {
325 | self.counters.invalid_enumeration_start_request += 1;
326 | return;
327 | };
328 |
329 | self.counters.enumeration_start_requests += 1;
330 |
331 | self.sink.enumeration_started(gateway_id);
332 | }
333 |
334 | fn identify_response(&mut self, frame: Frame) {
335 | let Address::From(gateway_id) = frame.address else {
336 | self.counters.invalid_identify_responses += 1;
337 | return;
338 | };
339 |
340 | let Ok(response) = IdentifyResponse::ref_from_bytes(frame.payload.as_ref()) else {
341 | self.counters.invalid_identify_responses += 1;
342 | return;
343 | };
344 |
345 | self.counters.identify_responses += 1;
346 |
347 | self.sink
348 | .gateway_identity_observed(gateway_id, response.pv_long_address);
349 | }
350 |
351 | fn enumeration_response(&mut self, frame: Frame) {
352 | let Address::From(gateway_id) = frame.address else {
353 | self.counters.invalid_enumeration_responses += 1;
354 | return;
355 | };
356 |
357 | let Ok(response) = IdentifyResponse::ref_from_bytes(frame.payload.as_ref()) else {
358 | self.counters.invalid_enumeration_responses += 1;
359 | return;
360 | };
361 |
362 | self.counters.enumeration_responses += 1;
363 |
364 | self.sink
365 | .gateway_identity_observed(gateway_id, response.pv_long_address);
366 | }
367 |
368 | pub fn version_response(&mut self, frame: Frame) {
369 | let Address::From(gateway_id) = frame.address else {
370 | self.counters.invalid_version_responses += 1;
371 | return;
372 | };
373 |
374 | let version = match std::str::from_utf8(frame.payload.as_ref()) {
375 | Ok(str) if !str.is_empty() => str,
376 | _ => {
377 | self.counters.invalid_version_responses += 1;
378 | return;
379 | }
380 | };
381 |
382 | self.counters.version_responses += 1;
383 | self.sink.gateway_version_observed(gateway_id, version);
384 | }
385 | }
386 |
387 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
388 | pub struct Counters {
389 | /// The number of received frames with an unknown frame type.
390 | pub unhandled_frame_type: u64,
391 | pub invalid_receive_request: u64,
392 | pub receive_requests: u64,
393 | pub invalid_receive_responses: u64,
394 | pub receive_response_from_unknown_gateway: u64,
395 | pub receive_responses: u64,
396 | pub receive_packets: u64,
397 | pub receive_packet_too_short: u64,
398 | pub invalid_command_requests: u64,
399 | pub retransmitted_command_requests: u64,
400 | pub command_requests: u64,
401 | pub invalid_command_responses: u64,
402 | pub retransmitted_command_responses: u64,
403 | pub command_responses: u64,
404 | pub ping_requests: u64,
405 | pub ping_responses: u64,
406 | pub enumeration_start_requests: u64,
407 | pub invalid_enumeration_start_request: u64,
408 | pub enumeration_start_responses: u64,
409 | pub enumeration_requests: u64,
410 | pub enumeration_responses: u64,
411 | pub invalid_enumeration_responses: u64,
412 | pub version_requests: u64,
413 | pub version_responses: u64,
414 | pub invalid_version_responses: u64,
415 | pub enumeration_end_requests: u64,
416 | pub enumeration_end_responses: u64,
417 | pub invalid_enumeration_end_responses: u64,
418 | pub assign_gateway_id_requests: u64,
419 | pub assign_gateway_id_responses: u64,
420 | pub identify_requests: u64,
421 | pub identify_responses: u64,
422 | pub invalid_identify_responses: u64,
423 | }
424 |
425 | #[cfg(test)]
426 | mod tests {
427 | use super::*;
428 | use crate::gateway;
429 | use crate::gateway::link::{Sink, Type};
430 | use crate::pv::LongAddress;
431 |
432 | #[derive(Debug, Clone, Eq, PartialEq)]
433 | enum Event {
434 | EnumerationStarted {
435 | enumeration_gateway_id: GatewayID,
436 | },
437 | GatewayIdentityObserved {
438 | gateway_id: GatewayID,
439 | address: LongAddress,
440 | },
441 | GatewayVersionObserved {
442 | gateway_id: GatewayID,
443 | version: String,
444 | },
445 | EnumerationEnded {
446 | gateway_id: GatewayID,
447 | },
448 | GatewaySlotCounterCaptured {
449 | gateway_id: GatewayID,
450 | },
451 | GatewaySlotCounterObserved {
452 | gateway_id: GatewayID,
453 | slot_counter: SlotCounter,
454 | },
455 | PacketReceived {
456 | gateway_id: GatewayID,
457 | header: ReceivedPacketHeader,
458 | data: Vec,
459 | },
460 | CommandExecuted {
461 | gateway_id: GatewayID,
462 | request: (PacketType, Vec),
463 | response: (PacketType, Vec),
464 | },
465 | }
466 | use Event::*;
467 |
468 | #[derive(Debug, Default)]
469 | struct TestSink(Vec);
470 | impl super::Sink for TestSink {
471 | fn enumeration_started(&mut self, enumeration_gateway_id: GatewayID) {
472 | self.0.push(EnumerationStarted {
473 | enumeration_gateway_id,
474 | });
475 | }
476 |
477 | fn gateway_identity_observed(&mut self, gateway_id: GatewayID, address: LongAddress) {
478 | self.0.push(GatewayIdentityObserved {
479 | gateway_id,
480 | address,
481 | })
482 | }
483 |
484 | fn gateway_version_observed(&mut self, gateway_id: GatewayID, version: &str) {
485 | self.0.push(GatewayVersionObserved {
486 | gateway_id,
487 | version: version.into(),
488 | })
489 | }
490 |
491 | fn enumeration_ended(&mut self, gateway_id: GatewayID) {
492 | self.0.push(EnumerationEnded { gateway_id });
493 | }
494 |
495 | fn gateway_slot_counter_captured(&mut self, gateway_id: GatewayID) {
496 | self.0.push(GatewaySlotCounterCaptured { gateway_id });
497 | }
498 |
499 | fn gateway_slot_counter_observed(
500 | &mut self,
501 | gateway_id: GatewayID,
502 | slot_counter: SlotCounter,
503 | ) {
504 | self.0.push(GatewaySlotCounterObserved {
505 | gateway_id,
506 | slot_counter,
507 | });
508 | }
509 |
510 | fn packet_received(
511 | &mut self,
512 | gateway_id: GatewayID,
513 | header: &ReceivedPacketHeader,
514 | data: &[u8],
515 | ) {
516 | self.0.push(PacketReceived {
517 | gateway_id,
518 | header: header.clone(),
519 | data: data.into(),
520 | })
521 | }
522 |
523 | fn command_executed(
524 | &mut self,
525 | gateway_id: GatewayID,
526 | request: (PacketType, &[u8]),
527 | response: (PacketType, &[u8]),
528 | ) {
529 | self.0.push(CommandExecuted {
530 | gateway_id,
531 | request: (request.0, request.1.into()),
532 | response: (response.0, response.1.into()),
533 | })
534 | }
535 | }
536 |
537 | #[test]
538 | fn unhandled_frame_type() {
539 | let mut rx = Receiver::new(TestSink::default());
540 | rx.frame(Frame {
541 | address: 0x1201.into(),
542 | frame_type: Type(0xffff),
543 | payload: vec![],
544 | });
545 |
546 | assert_eq!(&rx.sink().0, &[]);
547 | assert_eq!(
548 | rx.counters(),
549 | &Counters {
550 | unhandled_frame_type: 1,
551 | ..Default::default()
552 | }
553 | );
554 | }
555 |
556 | #[test]
557 | fn reset_counters() {
558 | let mut rx = Receiver::new(TestSink::default());
559 |
560 | assert_eq!(rx.counters(), &Counters::default());
561 |
562 | rx.frame(Frame {
563 | address: 0x1201.into(),
564 | frame_type: Type(0xffff),
565 | payload: vec![],
566 | });
567 | assert_ne!(rx.counters(), &Counters::default());
568 |
569 | rx.reset_counters();
570 | assert_eq!(rx.counters(), &Counters::default());
571 | }
572 |
573 | #[test]
574 | fn enumeration_sequence() {
575 | // Receive the exchange from the doc
576 | let mut rx = gateway::link::Receiver::new(Receiver::new(TestSink::default()));
577 | rx.extend_from_slice(crate::test_data::ENUMERATION_SEQUENCE);
578 |
579 | assert_eq!(
580 | &rx.sink().sink().0,
581 | &[
582 | EnumerationStarted {
583 | enumeration_gateway_id: GatewayID::try_from(0x1235).unwrap()
584 | },
585 | EnumerationStarted {
586 | enumeration_gateway_id: GatewayID::try_from(0x1235).unwrap()
587 | },
588 | EnumerationStarted {
589 | enumeration_gateway_id: GatewayID::try_from(0x1235).unwrap()
590 | },
591 | EnumerationStarted {
592 | enumeration_gateway_id: GatewayID::try_from(0x1235).unwrap()
593 | },
594 | EnumerationStarted {
595 | enumeration_gateway_id: GatewayID::try_from(0x1235).unwrap()
596 | },
597 | GatewayIdentityObserved {
598 | gateway_id: GatewayID::try_from(0x1235).unwrap(),
599 | address: LongAddress([0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16])
600 | },
601 | GatewayIdentityObserved {
602 | gateway_id: GatewayID::try_from(0x1201).unwrap(),
603 | address: LongAddress([0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16])
604 | },
605 | GatewayIdentityObserved {
606 | gateway_id: GatewayID::try_from(0x1202).unwrap(),
607 | address: LongAddress([0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16])
608 | },
609 | GatewayIdentityObserved {
610 | gateway_id: GatewayID::try_from(0x1201).unwrap(),
611 | address: LongAddress([0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16])
612 | },
613 | GatewayVersionObserved {
614 | gateway_id: GatewayID::try_from(0x1201).unwrap(),
615 | version: "Mgate Version G8.59\rJul 6 2020\r16:51:51\rGW-H158.4.3S0.12\r"
616 | .into()
617 | },
618 | EnumerationEnded {
619 | gateway_id: GatewayID::try_from(0x1201).unwrap()
620 | },
621 | ]
622 | );
623 | assert_eq!(
624 | rx.sink().counters(),
625 | &Counters {
626 | unhandled_frame_type: 2,
627 | ping_requests: 2,
628 | ping_responses: 2,
629 | enumeration_start_requests: 5,
630 | enumeration_start_responses: 5,
631 | enumeration_requests: 6,
632 | enumeration_responses: 1,
633 | version_requests: 1,
634 | version_responses: 1,
635 | enumeration_end_requests: 1,
636 | enumeration_end_responses: 1,
637 | assign_gateway_id_requests: 2,
638 | assign_gateway_id_responses: 2,
639 | identify_requests: 3,
640 | identify_responses: 3,
641 | ..Default::default()
642 | }
643 | );
644 | }
645 | }
646 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![doc = include_str!("../README.md")]
2 |
3 | pub mod barcode;
4 | pub mod gateway;
5 | pub mod pv;
6 |
7 | pub mod capture;
8 |
9 | pub mod config;
10 | pub mod observer;
11 |
12 | #[cfg(test)]
13 | pub mod test_data;
14 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use clap::{Args, Parser, Subcommand};
2 | use log::LevelFilter;
3 | use std::collections::btree_map::Entry;
4 | use std::collections::BTreeMap;
5 | use std::io::{Read, Write};
6 | use std::process::exit;
7 | use taptap::gateway::physical::Connection;
8 | use taptap::gateway::{physical, Frame, GatewayID};
9 | use taptap::pv::application::{NodeTableResponseEntry, PowerReport, TopologyReport};
10 | use taptap::pv::network::{NodeAddress, ReceivedPacketHeader};
11 | use taptap::pv::{LongAddress, NodeID, PacketType, SlotCounter};
12 | use taptap::{config, gateway, pv};
13 |
14 | #[derive(Parser, Debug, Clone)]
15 | #[command(version, about, long_about = None)]
16 | #[command(propagate_version = true)]
17 | struct Cli {
18 | #[command(subcommand)]
19 | command: Commands,
20 | }
21 |
22 | #[derive(Subcommand, Debug, Clone)]
23 | enum Commands {
24 | #[cfg(feature = "serialport")]
25 | ListSerialPorts,
26 |
27 | /// Observe the system, extracting data as it runs
28 | Observe {
29 | #[command(flatten)]
30 | source: Source,
31 | },
32 |
33 | /// Peek at the raw data flowing at the gateway physical layer
34 | PeekBytes {
35 | #[command(flatten)]
36 | source: Source,
37 | /// Print raw binary bytes without escaping
38 | #[arg(long)]
39 | raw: bool,
40 | },
41 |
42 | /// Peek at the assembled frames at the gateway link layer
43 | PeekFrames {
44 | #[command(flatten)]
45 | source: Source,
46 | },
47 |
48 | /// Peek at the gateway transport and PV application layer activity
49 | PeekActivity {
50 | #[command(flatten)]
51 | source: Source,
52 | },
53 | }
54 |
55 | #[derive(Args, Debug, Clone)]
56 | #[group(required = true, multiple = false)]
57 | struct Source {
58 | /// The name of the serial port (try `taptap list-serial-ports`)
59 | #[arg(long, group = "mode", value_name = "SERIAL-PORT")]
60 | #[cfg(feature = "serialport")]
61 | serial: Option,
62 |
63 | /// The IP or hostname which is providing serial-over-TCP service
64 | #[arg(long, group = "mode", value_name = "DESTINATION")]
65 | tcp: Option,
66 |
67 | // If --tcp is specified, the port to which to connect
68 | #[arg(long, requires = "tcp", default_value_t = 7160)]
69 | port: u16,
70 | }
71 |
72 | impl Source {
73 | fn open(&self) -> Box {
74 | let src = config::SourceConfig::from(self.clone());
75 | match src.open() {
76 | Ok(s) => s,
77 | Err(e) => {
78 | log::error!("error opening source: {}", e);
79 | exit(2);
80 | }
81 | }
82 | }
83 | }
84 |
85 | impl From for config::SourceConfig {
86 | fn from(value: Source) -> Self {
87 | #[cfg(feature = "serialport")]
88 | if let Some(name) = value.serial {
89 | return config::SerialSourceConfig { name }.into();
90 | }
91 |
92 | match (value.tcp,) {
93 | (Some(name),) => config::TcpConnectionConfig {
94 | hostname: name,
95 | port: value.port,
96 | mode: config::ConnectionMode::ReadOnly,
97 | }
98 | .into(),
99 | _ => {
100 | // clap assertions should prevent this
101 | panic!("a source must be specified");
102 | }
103 | }
104 | }
105 | }
106 |
107 | fn main() {
108 | let cli = Cli::parse();
109 | env_logger::Builder::new()
110 | .filter_level(LevelFilter::Info)
111 | .parse_default_env()
112 | .init();
113 |
114 | match cli.command {
115 | Commands::PeekBytes { source, raw } => {
116 | let source = source.open();
117 | peek_bytes(source, raw);
118 | }
119 |
120 | Commands::PeekFrames { source } => {
121 | let source = source.open();
122 | peek_frames(source);
123 | }
124 |
125 | Commands::PeekActivity { source } => {
126 | let source = source.open();
127 | peek_activity(source);
128 | }
129 |
130 | #[cfg(feature = "serialport")]
131 | Commands::ListSerialPorts => {
132 | list_serial_ports();
133 | }
134 |
135 | Commands::Observe { source } => {
136 | let source = source.open();
137 | observe(source)
138 | }
139 | }
140 | }
141 |
142 | fn peek_bytes(mut conn: Box, raw: bool) {
143 | let mut buffer = [0u8; 1024];
144 | let mut last_was_7e = false;
145 |
146 | loop {
147 | let slice = match conn.read(&mut buffer) {
148 | Ok(n) => &buffer[0..n],
149 | Err(e) => {
150 | log::error!("error reading: {}", e);
151 | exit(1);
152 | }
153 | };
154 |
155 | if slice.is_empty() {
156 | return;
157 | }
158 |
159 | let mut out = std::io::stdout().lock();
160 | if raw {
161 | out.write_all(slice).unwrap();
162 | } else {
163 | let mut formatted = Vec::with_capacity(4 * slice.len());
164 | for byte in slice {
165 | let sep = if last_was_7e && *byte == 0x08 {
166 | '\n'
167 | } else {
168 | ' '
169 | };
170 | write!(&mut formatted, "{:02X}{}", byte, sep).unwrap();
171 | last_was_7e = *byte == 0x7e;
172 | }
173 |
174 | out.write_all(formatted.as_slice()).unwrap();
175 | }
176 | out.flush().unwrap();
177 | }
178 | }
179 |
180 | fn peek_frames(mut conn: Box) {
181 | let mut buffer = [0u8; 1024];
182 |
183 | struct Sink;
184 | impl taptap::gateway::link::Sink for Sink {
185 | fn frame(&mut self, frame: Frame) {
186 | println!("{:?}", frame);
187 | }
188 | }
189 |
190 | let mut rx = taptap::gateway::link::Receiver::new(Sink);
191 |
192 | loop {
193 | let slice = match conn.read(&mut buffer) {
194 | Ok(n) => &buffer[0..n],
195 | Err(e) => {
196 | log::error!("error reading: {}", e);
197 | exit(1);
198 | }
199 | };
200 |
201 | if slice.is_empty() {
202 | return;
203 | }
204 |
205 | rx.extend_from_slice(slice);
206 | }
207 | }
208 |
209 | fn peek_activity(mut conn: Box) {
210 | #[derive(Default)]
211 | struct Sink {
212 | slot_counters: BTreeMap,
213 | }
214 | impl gateway::transport::Sink for Sink {
215 | fn enumeration_started(&mut self, enumeration_gateway_id: GatewayID) {
216 | log::info!("enumeration started (at {:?})", enumeration_gateway_id);
217 | }
218 |
219 | fn gateway_identity_observed(&mut self, gateway_id: GatewayID, address: LongAddress) {
220 | log::info!(
221 | "gateway identity observed: {:?} = {:?}",
222 | gateway_id,
223 | address
224 | );
225 | }
226 |
227 | fn gateway_version_observed(&mut self, gateway_id: GatewayID, version: &str) {
228 | log::info!("gateway version observed: {:?} = {:?}", gateway_id, version);
229 | }
230 |
231 | fn enumeration_ended(&mut self, gateway_id: GatewayID) {
232 | log::info!("enumeration ended: {:?}", gateway_id);
233 | }
234 |
235 | fn gateway_slot_counter_captured(&mut self, _gateway_id: GatewayID) {}
236 |
237 | fn gateway_slot_counter_observed(
238 | &mut self,
239 | gateway_id: GatewayID,
240 | slot_counter: SlotCounter,
241 | ) {
242 | let print = match self.slot_counters.entry(gateway_id) {
243 | Entry::Vacant(e) => {
244 | e.insert(slot_counter);
245 | true
246 | }
247 | Entry::Occupied(mut e) => {
248 | let last = e.get();
249 | let print = last.epoch() != slot_counter.epoch()
250 | || (last.0.get() & 0x3fff) / 1000 != (slot_counter.0.get() & 0x3fff) / 1000;
251 | e.insert(slot_counter);
252 | print
253 | }
254 | };
255 |
256 | if print {
257 | log::info!("slot counter: {:?} {:?}", gateway_id, slot_counter)
258 | }
259 | }
260 |
261 | fn packet_received(
262 | &mut self,
263 | gateway_id: GatewayID,
264 | header: &ReceivedPacketHeader,
265 | data: &[u8],
266 | ) {
267 | match header.packet_type {
268 | PacketType::STRING_RESPONSE
269 | | PacketType::POWER_REPORT
270 | | PacketType::TOPOLOGY_REPORT => return,
271 | _ => {}
272 | }
273 | log::info!("packet received: {:?} {:?} {:?}", gateway_id, header, data);
274 | }
275 |
276 | fn command_executed(
277 | &mut self,
278 | gateway_id: GatewayID,
279 | request: (PacketType, &[u8]),
280 | response: (PacketType, &[u8]),
281 | ) {
282 | match request.0 {
283 | PacketType::STRING_REQUEST => return,
284 | PacketType::NODE_TABLE_REQUEST => return,
285 | _ => {}
286 | }
287 |
288 | log::info!(
289 | "command executed: {:?} {:?} {:?} => {:?} {:?}",
290 | gateway_id,
291 | request.0,
292 | request.1,
293 | response.0,
294 | response.1
295 | );
296 | }
297 | }
298 | impl pv::application::Sink for Sink {
299 | fn string_request(&mut self, gateway_id: GatewayID, pv_node_id: NodeID, request: &str) {
300 | log::info!(
301 | "string request: {:?} {:?} {:?}",
302 | gateway_id,
303 | pv_node_id,
304 | request
305 | );
306 | }
307 |
308 | fn string_response(&mut self, gateway_id: GatewayID, pv_node_id: NodeID, response: &str) {
309 | log::info!(
310 | "string response: {:?} {:?} {:?}",
311 | gateway_id,
312 | pv_node_id,
313 | response
314 | );
315 | }
316 |
317 | fn node_table_page(
318 | &mut self,
319 | gateway_id: GatewayID,
320 | start_address: NodeAddress,
321 | nodes: &[NodeTableResponseEntry],
322 | ) {
323 | log::info!(
324 | "node table page: {:?} start {:?} {:?}",
325 | gateway_id,
326 | start_address,
327 | nodes
328 | );
329 | }
330 |
331 | fn topology_report(
332 | &mut self,
333 | gateway_id: GatewayID,
334 | pv_node_id: NodeID,
335 | topology_report: &TopologyReport,
336 | ) {
337 | log::info!(
338 | "topology report: {:?} {:?} {:?}",
339 | gateway_id,
340 | pv_node_id,
341 | topology_report
342 | );
343 | }
344 |
345 | fn power_report(
346 | &mut self,
347 | gateway_id: GatewayID,
348 | pv_node_id: NodeID,
349 | power_report: &PowerReport,
350 | ) {
351 | log::info!(
352 | "power report: {:?} {:?} {:?}",
353 | gateway_id,
354 | pv_node_id,
355 | power_report
356 | );
357 | }
358 | }
359 |
360 | let mut rx = gateway::link::Receiver::new(gateway::transport::Receiver::new(
361 | pv::application::Receiver::new(Sink::default()),
362 | ));
363 |
364 | let mut buffer = [0u8; 1024];
365 | loop {
366 | let slice = match conn.read(&mut buffer) {
367 | Ok(n) => &buffer[0..n],
368 | Err(e) => {
369 | log::error!("error reading: {}", e);
370 | exit(1);
371 | }
372 | };
373 |
374 | if slice.is_empty() {
375 | return;
376 | }
377 |
378 | rx.extend_from_slice(slice);
379 | }
380 | }
381 | #[cfg(feature = "serialport")]
382 | fn list_serial_ports() {
383 | use serialport::SerialPortType;
384 |
385 | let mut ports = match physical::serialport::PortInfo::list() {
386 | Ok(ports) => ports,
387 | Err(e) => {
388 | log::error!("error listing serial ports: {}", e);
389 | exit(1);
390 | }
391 | };
392 |
393 | ports.sort_by_cached_key(|port| port.name().to_owned());
394 |
395 | if ports.is_empty() {
396 | println!("No serial ports detected.")
397 | } else {
398 | println!("Detected:");
399 | }
400 |
401 | for port in ports {
402 | println!(" --serial {}", port.name());
403 | match port.port_type() {
404 | SerialPortType::UsbPort(usb) if usb.manufacturer.is_some() && usb.product.is_some() => {
405 | println!(
406 | " USB {:04x}:{:04x} ({} {})",
407 | usb.pid,
408 | usb.vid,
409 | usb.manufacturer.as_ref().unwrap(),
410 | usb.product.as_ref().unwrap()
411 | );
412 | }
413 | SerialPortType::UsbPort(usb) => {
414 | println!(" USB {:04x}:{:04x}", usb.pid, usb.vid);
415 | }
416 | SerialPortType::BluetoothPort => {
417 | println!(" Bluetooth");
418 | }
419 | _ => {}
420 | }
421 | }
422 | }
423 |
424 | fn observe(mut conn: Box) {
425 | let observer = taptap::observer::Observer::default();
426 | let mut rx = gateway::link::Receiver::new(gateway::transport::Receiver::new(
427 | pv::application::Receiver::new(observer),
428 | ));
429 |
430 | let mut buffer = [0u8; 1024];
431 | loop {
432 | let slice = match conn.read(&mut buffer) {
433 | Ok(n) => &buffer[0..n],
434 | Err(e) => {
435 | log::error!("error reading: {}", e);
436 | exit(1);
437 | }
438 | };
439 |
440 | if slice.is_empty() {
441 | return;
442 | }
443 |
444 | rx.extend_from_slice(slice);
445 | }
446 | }
447 |
--------------------------------------------------------------------------------
/src/observer.rs:
--------------------------------------------------------------------------------
1 | //! An observer which can monitor a controller <-> gateway network.
2 | //!
3 | //! ```text
4 | //! ┌───┐
5 | //! │TAP│◁ ─ ─ ─ …
6 | //! └───┘
7 | //! ▲
8 | //! │
9 | //! │
10 | //! ├──────┐
11 | //! ▼ ▼
12 | //! ┌───┐ ┌───┐
13 | //! │CCA│ │O_o│
14 | //! └───┘ └───┘
15 | //! ```
16 |
17 | use crate::gateway::link::GatewayID;
18 | use crate::pv::application::{NodeTableResponseEntry, TopologyReport};
19 | use crate::pv::link::SlotCounter;
20 | use crate::pv::network::{NodeAddress, ReceivedPacketHeader};
21 | use crate::pv::{LongAddress, NodeID, PacketType};
22 | use crate::{gateway, pv};
23 | use schemars::JsonSchema;
24 | use serde::{Deserialize, Serialize};
25 | use std::collections::btree_map::Entry;
26 | use std::collections::BTreeMap;
27 | use std::time::SystemTime;
28 |
29 | pub mod event;
30 |
31 | mod node_table;
32 | use node_table::{NodeTable, NodeTableBuilder};
33 |
34 | mod slot_clock;
35 | use slot_clock::SlotClock;
36 |
37 | /// An observer, monitoring a controller interacting with one or more TAPs via an RS-485 interface.
38 | #[derive(Debug)]
39 | pub struct Observer {
40 | persistent_state: PersistentState,
41 |
42 | enumeration_state: Option,
43 | captured_slot_counters: BTreeMap,
44 | slot_clocks: BTreeMap,
45 | node_table_builders: BTreeMap,
46 | }
47 |
48 | impl Default for Observer {
49 | fn default() -> Self {
50 | Self::from_persistent_state(PersistentState::default())
51 | }
52 | }
53 |
54 | impl Observer {
55 | pub fn from_persistent_state(persistent_state: PersistentState) -> Self {
56 | Observer {
57 | persistent_state,
58 | enumeration_state: None,
59 | captured_slot_counters: Default::default(),
60 | slot_clocks: Default::default(),
61 | node_table_builders: Default::default(),
62 | }
63 | }
64 |
65 | pub fn persistent_state(&self) -> &PersistentState {
66 | &self.persistent_state
67 | }
68 |
69 | fn gateway(&self, id: GatewayID) -> event::Gateway {
70 | let address = self.persistent_state.gateway_identities.get(&id).copied();
71 | event::Gateway { id, address }
72 | }
73 |
74 | fn node(&self, gateway_id: GatewayID, id: NodeID) -> event::Node {
75 | let address = self
76 | .persistent_state
77 | .gateway_node_tables
78 | .get(&gateway_id)
79 | .and_then(|node_table| node_table.0.get(&id))
80 | .copied();
81 |
82 | let barcode = address.map(|addr| addr.into());
83 |
84 | event::Node {
85 | id,
86 | address,
87 | barcode,
88 | }
89 | }
90 | }
91 |
92 | impl gateway::transport::Sink for Observer {
93 | fn enumeration_started(&mut self, enumeration_gateway_id: GatewayID) {
94 | self.enumeration_state = Some(EnumerationState {
95 | enumeration_gateway_id,
96 | gateway_identities: Default::default(),
97 | gateway_versions: Default::default(),
98 | });
99 | }
100 |
101 | fn gateway_identity_observed(&mut self, gateway_id: GatewayID, address: LongAddress) {
102 | if let Some(enumeration_state) = self.enumeration_state.as_mut() {
103 | // We're enumerating
104 | // Delegate
105 | enumeration_state.gateway_identity_observed(gateway_id, address);
106 | } else {
107 | // Accept the identity as-is
108 | self.persistent_state
109 | .gateway_identities
110 | .insert(gateway_id, address);
111 | }
112 | }
113 |
114 | fn gateway_version_observed(&mut self, gateway_id: GatewayID, version: &str) {
115 | let version = version.to_owned();
116 |
117 | if let Some(enumeration_state) = self.enumeration_state.as_mut() {
118 | enumeration_state
119 | .gateway_versions
120 | .insert(gateway_id, version);
121 | } else {
122 | self.persistent_state
123 | .gateway_versions
124 | .insert(gateway_id, version);
125 | }
126 | }
127 |
128 | fn enumeration_ended(&mut self, _gateway_id: GatewayID) {
129 | // We're done enumerating
130 | // Did we catch the whole exchange?
131 | if let Some(enumeration_state) = self.enumeration_state.take() {
132 | // Accept the gateway information learned during enumeration as a replacement for our
133 | // existing state
134 | self.persistent_state.gateway_identities = enumeration_state.gateway_identities;
135 | self.persistent_state.gateway_versions = enumeration_state.gateway_versions;
136 | }
137 | }
138 |
139 | fn gateway_slot_counter_captured(&mut self, gateway_id: GatewayID) {
140 | self.captured_slot_counters
141 | .insert(gateway_id, SystemTime::now());
142 | }
143 |
144 | fn gateway_slot_counter_observed(&mut self, gateway_id: GatewayID, slot_counter: SlotCounter) {
145 | let Some(time) = self.captured_slot_counters.remove(&gateway_id) else {
146 | return;
147 | };
148 |
149 | match self.slot_clocks.entry(gateway_id) {
150 | Entry::Vacant(e) => {
151 | if let Ok(clock) = SlotClock::new(slot_counter, time) {
152 | e.insert(clock);
153 | }
154 | }
155 | Entry::Occupied(mut e) => {
156 | e.get_mut().set(slot_counter, time).ok();
157 | }
158 | }
159 | }
160 |
161 | fn packet_received(
162 | &mut self,
163 | _gateway_id: GatewayID,
164 | _header: &ReceivedPacketHeader,
165 | _data: &[u8],
166 | ) {
167 | }
168 |
169 | fn command_executed(
170 | &mut self,
171 | _gateway_id: GatewayID,
172 | _request: (PacketType, &[u8]),
173 | _response: (PacketType, &[u8]),
174 | ) {
175 | }
176 | }
177 |
178 | impl pv::application::Sink for Observer {
179 | fn string_request(&mut self, _gateway_id: GatewayID, _pv_node_id: NodeID, _request: &str) {}
180 |
181 | fn string_response(&mut self, _gateway_id: GatewayID, _pv_node_id: NodeID, _response: &str) {}
182 |
183 | fn node_table_page(
184 | &mut self,
185 | gateway_id: GatewayID,
186 | start_address: NodeAddress,
187 | nodes: &[NodeTableResponseEntry],
188 | ) {
189 | let builder = self.node_table_builders.entry(gateway_id).or_default();
190 |
191 | if let Some(new_table) = builder.push(start_address, nodes) {
192 | self.persistent_state
193 | .gateway_node_tables
194 | .insert(gateway_id, new_table);
195 | }
196 | }
197 |
198 | fn topology_report(
199 | &mut self,
200 | _gateway_id: GatewayID,
201 | _pv_node_id: NodeID,
202 | _topology_report: &TopologyReport,
203 | ) {
204 | }
205 |
206 | fn power_report(
207 | &mut self,
208 | gateway_id: GatewayID,
209 | pv_node_id: NodeID,
210 | power_report: &pv::application::PowerReport,
211 | ) {
212 | let Some(slot_clock) = self.slot_clocks.get(&gateway_id) else {
213 | log::error!(
214 | "discarding power report from gateway {:?} due to missing slot clock: {:?}",
215 | gateway_id,
216 | power_report
217 | );
218 | return;
219 | };
220 |
221 | let Ok(event) = event::PowerReportEvent::new(
222 | self.gateway(gateway_id),
223 | self.node(gateway_id, pv_node_id),
224 | slot_clock,
225 | power_report,
226 | ) else {
227 | log::error!(
228 | "discarding power report from gateway {:?} due to invalid slot counter: {:?}",
229 | gateway_id,
230 | power_report
231 | );
232 | return;
233 | };
234 |
235 | println!("{}", serde_json::to_string(&event).unwrap());
236 | }
237 | }
238 |
239 | /// Persistent state of an observed network.
240 | ///
241 | /// Information like hardware addresses and version numbers are exchanged infrequently. This data
242 | /// is captured and stored in `PersistentState`.
243 | #[derive(Debug, Clone, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize)]
244 | pub struct PersistentState {
245 | gateway_node_tables: BTreeMap,
246 |
247 | gateway_identities: BTreeMap,
248 | gateway_versions: BTreeMap,
249 | }
250 |
251 | #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
252 | struct EnumerationState {
253 | enumeration_gateway_id: GatewayID,
254 | gateway_identities: BTreeMap,
255 | gateway_versions: BTreeMap,
256 | }
257 |
258 | impl EnumerationState {
259 | fn gateway_identity_observed(&mut self, gateway: GatewayID, address: LongAddress) {
260 | // Is this a persistent ID?
261 | if gateway == self.enumeration_gateway_id {
262 | // No, it's the enumeration address
263 | // Discard this response, since we'll get a persistent one shortly
264 | return;
265 | }
266 |
267 | // Store the identity
268 | self.gateway_identities.insert(gateway, address);
269 | }
270 | }
271 |
272 | #[cfg(test)]
273 | mod tests;
274 |
--------------------------------------------------------------------------------
/src/observer/event.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 | use crate::barcode::Barcode;
3 | use crate::pv;
4 | use crate::pv::link::InvalidSlotNumber;
5 | use crate::pv::physical::RSSI;
6 | use chrono::{DateTime, Local};
7 |
8 | /// An event produced by an observer.
9 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
10 | #[serde(rename = "snake_case")]
11 | pub enum Event {
12 | PowerReport(PowerReportEvent),
13 | }
14 |
15 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
16 | pub struct Gateway {
17 | /// The gateway's link layer ID.
18 | ///
19 | /// This value can change over time and is duplicated between different systems, but it is
20 | /// always present.
21 | pub id: gateway::link::GatewayID,
22 |
23 | /// The gateway's hardware address.
24 | ///
25 | /// This value is permanent and globally unique, but it is not always known.
26 | #[serde(default, skip_serializing_if = "Option::is_none")]
27 | pub address: Option,
28 | }
29 |
30 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
31 | pub struct Node {
32 | /// The node's ID.
33 | ///
34 | /// This value can change over time and is duplicated between different gateways, but it is
35 | /// always present.
36 | pub id: pv::NodeID,
37 |
38 | /// The node's hardware address.
39 | ///
40 | /// This value is permanent and globally unique, but it is not always known.
41 | #[serde(default, skip_serializing_if = "Option::is_none")]
42 | pub address: Option,
43 |
44 | /// The node's barcode.
45 | ///
46 | /// This value is permanent and globally unique, but it is not always known.
47 | #[serde(default, skip_serializing_if = "Option::is_none")]
48 | pub barcode: Option,
49 | }
50 |
51 | #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
52 | pub struct PowerReportEvent {
53 | /// The gateway through which the power report was received.
54 | pub gateway: Gateway,
55 | /// The node sending the power report.
56 | pub node: Node,
57 | /// The time at which this measurement was taken.
58 | pub timestamp: DateTime,
59 | pub voltage_in: f64,
60 | pub voltage_out: f64,
61 | pub current: f64,
62 | pub dc_dc_duty_cycle: f64,
63 | pub temperature: f64,
64 | pub rssi: RSSI,
65 | }
66 |
67 | impl PowerReportEvent {
68 | pub fn new(
69 | gateway: Gateway,
70 | node: Node,
71 | slot_clock: &SlotClock,
72 | report: &pv::application::PowerReport,
73 | ) -> Result {
74 | let timestamp = slot_clock.get(report.slot_counter)?;
75 |
76 | let (voltage_in, voltage_out) = report.voltage_in_and_voltage_out.into();
77 | let (current, temperature) = report.current_and_temperature.into();
78 |
79 | // XXX: is it correct to sign-extend temperature?
80 | // How are below-freezing temperatures reported? (This assumes two's complement.)
81 | let temperature = if temperature & 0x800 == 0 {
82 | temperature
83 | } else {
84 | temperature | 0xF000
85 | } as i16;
86 |
87 | Ok(Self {
88 | gateway,
89 | node,
90 | timestamp: timestamp.into(),
91 | voltage_in: voltage_in as f64 / 20.0, //* 0.05,
92 | voltage_out: voltage_out as f64 / 10.0, // * 0.10,
93 | dc_dc_duty_cycle: report.dc_dc_duty_cycle as f64 / 255.0,
94 | current: current as f64 / 200.0, // * 0.005,
95 | temperature: temperature as f64 / 10.0, // * 0.01,
96 | rssi: report.rssi,
97 | })
98 | }
99 | }
100 |
101 | #[cfg(test)]
102 | mod tests {
103 | use super::*;
104 | use crate::pv::application::{PowerReport, U12Pair};
105 |
106 | #[test]
107 | fn negative_temperature() {
108 | let gateway = Gateway {
109 | id: 1.try_into().unwrap(),
110 | address: None,
111 | };
112 | let node = Node {
113 | id: 1.try_into().unwrap(),
114 | address: None,
115 | barcode: None,
116 | };
117 |
118 | let rssi = RSSI(100);
119 | let timestamp = SystemTime::now();
120 | let slot_counter = SlotCounter::from(0);
121 | let slot_clock = SlotClock::new(slot_counter, timestamp).unwrap();
122 |
123 | let power_report = PowerReport {
124 | voltage_in_and_voltage_out: U12Pair::try_from((500, 250)).unwrap(),
125 | dc_dc_duty_cycle: 255,
126 | current_and_temperature: U12Pair::try_from((200, 0xfff)).unwrap(),
127 | unknown: [0, 0, 0],
128 | slot_counter,
129 | rssi,
130 | };
131 |
132 | let power_report_event =
133 | PowerReportEvent::new(gateway, node, &slot_clock, &power_report).unwrap();
134 |
135 | let actual = serde_json::to_string(&power_report_event).unwrap();
136 | let expected = serde_json::to_string(&PowerReportEvent {
137 | gateway,
138 | node,
139 | timestamp: timestamp.into(),
140 | voltage_in: 25.0,
141 | voltage_out: 25.0,
142 | current: 1.00,
143 | dc_dc_duty_cycle: 1.0,
144 | temperature: -0.1,
145 | rssi,
146 | })
147 | .unwrap();
148 | assert_eq!(actual, expected); // floats :|
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/observer/node_table.rs:
--------------------------------------------------------------------------------
1 | use crate::pv::application::NodeTableResponseEntry;
2 | use crate::pv::network::NodeAddress;
3 | use crate::pv::{LongAddress, NodeID};
4 | use schemars::{JsonSchema, Schema, SchemaGenerator};
5 | use serde::de::Error;
6 | use serde::{Deserialize, Deserializer, Serialize, Serializer};
7 | use std::borrow::Cow;
8 | use std::collections::BTreeMap;
9 |
10 | #[derive(Debug, Clone, Eq, PartialEq, Default)]
11 | pub struct NodeTable(pub(crate) BTreeMap);
12 |
13 | impl JsonSchema for NodeTable {
14 | fn schema_name() -> Cow<'static, str> {
15 | "NodeTable".into()
16 | }
17 |
18 | fn schema_id() -> Cow<'static, str> {
19 | concat!(module_path!(), "::NodeTable").into()
20 | }
21 |
22 | fn json_schema(gen: &mut SchemaGenerator) -> Schema {
23 | schemars::json_schema!({
24 | "type": "array",
25 | "uniqueItems": true,
26 | "items": gen.subschema_for::(),
27 | })
28 | }
29 | }
30 |
31 | #[derive(Debug, Clone, Eq, PartialEq, JsonSchema, Serialize, Deserialize)]
32 | struct NodeTableEntry {
33 | pub node_id: NodeID,
34 | pub long_address: LongAddress,
35 | }
36 |
37 | impl From<(NodeID, LongAddress)> for NodeTableEntry {
38 | fn from((node_id, long_address): (NodeID, LongAddress)) -> Self {
39 | Self {
40 | node_id,
41 | long_address,
42 | }
43 | }
44 | }
45 | impl From<(&NodeID, &LongAddress)> for NodeTableEntry {
46 | fn from((&node_id, &long_address): (&NodeID, &LongAddress)) -> Self {
47 | Self {
48 | node_id,
49 | long_address,
50 | }
51 | }
52 | }
53 | impl From for (NodeID, LongAddress) {
54 | fn from(value: NodeTableEntry) -> Self {
55 | (value.node_id, value.long_address)
56 | }
57 | }
58 |
59 | // Serialize as Vec
60 | impl Serialize for NodeTable {
61 | fn serialize(&self, serializer: S) -> Result
62 | where
63 | S: Serializer,
64 | {
65 | let entries: Vec = self.0.iter().map(NodeTableEntry::from).collect();
66 | entries.serialize(serializer)
67 | }
68 | }
69 |
70 | // Deserialize from Vec
71 | impl<'de> Deserialize<'de> for NodeTable {
72 | fn deserialize(deserializer: D) -> Result
73 | where
74 | D: Deserializer<'de>,
75 | {
76 | let entries = >::deserialize(deserializer)?;
77 | let len = entries.len();
78 | let output: Self = Self(
79 | entries
80 | .into_iter()
81 | .map(<(NodeID, LongAddress)>::from)
82 | .collect(),
83 | );
84 |
85 | if output.0.len() == len {
86 | Ok(output)
87 | } else {
88 | Err(D::Error::custom(
89 | "node_ids must be unique within a node table",
90 | ))
91 | }
92 | }
93 | }
94 |
95 | #[derive(Debug, Clone, Eq, PartialEq, Default)]
96 | pub struct NodeTableBuilder {
97 | expected_next: Option,
98 | table: NodeTable,
99 | }
100 |
101 | impl NodeTableBuilder {
102 | pub fn push(
103 | &mut self,
104 | start_address: NodeAddress,
105 | entries: &[NodeTableResponseEntry],
106 | ) -> Option {
107 | // Are we continuing an existing table?
108 | if NodeAddress::from(self.expected_next) != start_address {
109 | // Reset
110 | self.expected_next = Default::default();
111 | self.table = Default::default();
112 | if start_address == NodeAddress::ZERO {
113 | // We're mid-table
114 | // Ignore
115 | return None;
116 | }
117 | }
118 |
119 | // Insert all the records
120 | for entry in entries {
121 | let Ok(node_id) = entry.node_id.try_into() else {
122 | // Fail
123 | self.expected_next = None;
124 | return None;
125 | };
126 |
127 | // Insert the record
128 | self.table.0.insert(node_id, entry.long_address);
129 | }
130 |
131 | // This was the end of the table?
132 | if entries.is_empty() {
133 | // Take the table
134 | let mut table = Default::default();
135 | std::mem::swap(&mut self.table, &mut table);
136 |
137 | // Reset
138 | self.expected_next = None;
139 |
140 | // Return the table
141 | Some(table)
142 | } else {
143 | // There's more
144 | let last = self.table.0.last_entry().unwrap();
145 | self.expected_next = last.key().successor();
146 |
147 | // Did we wrap?
148 | if self.expected_next.is_none() {
149 | self.table = Default::default();
150 | }
151 |
152 | None
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/observer/slot_clock.rs:
--------------------------------------------------------------------------------
1 | use crate::pv::link::InvalidSlotNumber;
2 | use crate::pv::SlotCounter;
3 | use std::time::{Duration, SystemTime};
4 |
5 | /// A data structure collating absolute timestamps to slot counters.
6 | #[derive(Debug, Clone)]
7 | pub struct SlotClock {
8 | // SystemTime timestamp per thousand ticks, i.e. per ±5s, wrapping with the counter
9 | times: [SystemTime; 48],
10 | last_index: usize,
11 | last_time: SystemTime,
12 | }
13 |
14 | const NOMINAL_DURATION_PER_SLOT: Duration = Duration::from_millis(5);
15 | const NOMINAL_DURATION_PER_INDEX: Duration = Duration::from_millis(5 * 1000);
16 |
17 | impl SlotClock {
18 | pub fn new(slot_counter: SlotCounter, time: SystemTime) -> Result {
19 | let (index, offset) = Self::index_and_offset(slot_counter)?;
20 | let index_time = time - offset;
21 |
22 | let mut table = Self {
23 | times: [index_time; 48],
24 | last_index: index,
25 | last_time: time,
26 | };
27 |
28 | // Walk backwards, assuming nominal time for each
29 | let mut index_time = index_time;
30 | let mut i = index;
31 | loop {
32 | // Subtract one index
33 | i = (47 + i) % 48;
34 | if i == index {
35 | // Go around once
36 | break;
37 | }
38 |
39 | // Subtract one duration
40 | index_time -= NOMINAL_DURATION_PER_INDEX;
41 | // Assign
42 | table.times[i] = index_time;
43 | }
44 |
45 | Ok(table)
46 | }
47 |
48 | fn index_and_offset(slot_counter: SlotCounter) -> Result<(usize, Duration), InvalidSlotNumber> {
49 | slot_counter.slot_number().map(|n| {
50 | let absolute_slot =
51 | (slot_counter.epoch() as u8 as usize) * 12000 + (u16::from(n)) as usize;
52 | let index = absolute_slot / 1000;
53 | let offset = NOMINAL_DURATION_PER_SLOT * (absolute_slot % 1000) as u32;
54 | (index, offset)
55 | })
56 | }
57 |
58 | pub fn set(
59 | &mut self,
60 | slot_counter: SlotCounter,
61 | time: SystemTime,
62 | ) -> Result<(), InvalidSlotNumber> {
63 | let (index, offset) = Self::index_and_offset(slot_counter)?;
64 |
65 | if self.last_time > time {
66 | // Clock went backwards
67 | // Replace the table entirely
68 | log::warn!("time went backwards: {:?} => {:?}", self.last_time, time);
69 | *self = Self::new(slot_counter, time)?;
70 | return Ok(());
71 | } else if self.last_index != index {
72 | // Assign this index
73 | let index_time = time - offset;
74 |
75 | // Set the entry
76 | self.times[index] = index_time;
77 |
78 | // Walk backwards, assuming nominal time for each
79 | let mut index_time = index_time;
80 | let mut i = index;
81 | loop {
82 | // Subtract one index
83 | i = (47 + i) % 48;
84 | if i == self.last_index {
85 | // Don't clobber the last assigned slot
86 | break;
87 | }
88 |
89 | // Subtract one duration
90 | index_time -= NOMINAL_DURATION_PER_INDEX;
91 | // Assign
92 | self.times[i] = index_time;
93 | }
94 | } else {
95 | // Don't reassign this index
96 | }
97 |
98 | // Record this assignment
99 | self.last_index = index;
100 | self.last_time = time;
101 |
102 | Ok(())
103 | }
104 |
105 | pub fn get(&self, slot_counter: SlotCounter) -> Result {
106 | // TODO: interpolate for accuracy? Or don't, because measurements come in at thousands.
107 | let (index, offset) = Self::index_and_offset(slot_counter)?;
108 | Ok(self.times[index] + offset)
109 | }
110 | }
111 |
112 | #[cfg(test)]
113 | mod tests {
114 | use super::*;
115 |
116 | #[test]
117 | fn smoke() {
118 | // Pick a time and call it "x"
119 | let x = SystemTime::UNIX_EPOCH + Duration::from_secs(1723500000);
120 |
121 | // Assume a gateway told us it's 0xc000
122 | let mut clock = SlotClock::new(SlotCounter::from(0xc000), x).unwrap();
123 |
124 | // 0x8000 was one minute ago
125 | assert_eq!(
126 | clock.get(SlotCounter::from(0x8000)),
127 | Ok(x - Duration::from_secs(60))
128 | );
129 | // 0x4000 was two minutes ago
130 | assert_eq!(
131 | clock.get(SlotCounter::from(0x4000)),
132 | Ok(x - Duration::from_secs(120))
133 | );
134 | // 0x0000 was three minutes ago
135 | assert_eq!(
136 | clock.get(SlotCounter::from(0x0000)),
137 | Ok(x - Duration::from_secs(180))
138 | );
139 | // 0xc000 + 1000 was three minutes 55 seconds ago
140 | assert_eq!(
141 | clock.get(SlotCounter::from(0xc000 + 1000)),
142 | Ok(x - Duration::from_secs(180 + 55))
143 | );
144 |
145 | // Advance to 0xc000 + 1000 at 5 seconds later
146 | let later = x + Duration::from_secs(5);
147 | clock.set(SlotCounter::from(0xc000 + 1000), later).unwrap();
148 |
149 | // 0x8000 was one minute before x
150 | assert_eq!(
151 | clock.get(SlotCounter::from(0x8000)),
152 | Ok(x - Duration::from_secs(60))
153 | );
154 | // 0x4000 was two minutes before x
155 | assert_eq!(
156 | clock.get(SlotCounter::from(0x4000)),
157 | Ok(x - Duration::from_secs(120))
158 | );
159 | // 0x0000 was three minutes before x
160 | assert_eq!(
161 | clock.get(SlotCounter::from(0x0000)),
162 | Ok(x - Duration::from_secs(180))
163 | );
164 | // 0xc000 + 1000 is x + 5
165 | assert_eq!(
166 | clock.get(SlotCounter::from(0xc000 + 1000)),
167 | Ok(x + Duration::from_secs(5))
168 | );
169 | }
170 |
171 | #[test]
172 | fn index_and_offset() {
173 | assert_eq!(
174 | SlotClock::index_and_offset(SlotCounter::ZERO),
175 | Ok((0, Duration::from_millis(0)))
176 | );
177 | assert_eq!(
178 | SlotClock::index_and_offset(SlotCounter(999.into())),
179 | Ok((0, Duration::from_millis(999 * 5)))
180 | );
181 | assert_eq!(
182 | SlotClock::index_and_offset(SlotCounter(1000.into())),
183 | Ok((1, Duration::from_millis(0)))
184 | );
185 | assert_eq!(
186 | SlotClock::index_and_offset(SlotCounter(1999.into())),
187 | Ok((1, Duration::from_millis(999 * 5)))
188 | );
189 | assert_eq!(
190 | SlotClock::index_and_offset(SlotCounter(2000.into())),
191 | Ok((2, Duration::from_millis(0)))
192 | );
193 | assert_eq!(
194 | SlotClock::index_and_offset(SlotCounter(11999.into())),
195 | Ok((11, Duration::from_millis(999 * 5)))
196 | );
197 | assert_eq!(
198 | SlotClock::index_and_offset(SlotCounter(12000.into())),
199 | Err(InvalidSlotNumber(12000))
200 | );
201 | assert_eq!(
202 | SlotClock::index_and_offset(SlotCounter(0x4000.into())),
203 | Ok((12, Duration::from_millis(0)))
204 | );
205 | assert_eq!(
206 | SlotClock::index_and_offset(SlotCounter((0x4000 + 999).into())),
207 | Ok((12, Duration::from_millis(999 * 5)))
208 | );
209 | assert_eq!(
210 | SlotClock::index_and_offset(SlotCounter((0x4000 + 1000).into())),
211 | Ok((13, Duration::from_millis(0)))
212 | );
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/observer/tests.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | #[test]
4 | fn enumeration_sequence() {
5 | let mut rx = gateway::link::Receiver::new(gateway::transport::Receiver::new(
6 | pv::application::Receiver::new(Observer::default()),
7 | ));
8 |
9 | // Receive the exchange from the doc, in two parts
10 | let (left, right) = crate::test_data::ENUMERATION_SEQUENCE.split_at(300);
11 | rx.extend_from_slice(left);
12 | {
13 | let observer = rx.sink().sink().sink();
14 | assert!(observer.enumeration_state.is_some());
15 | assert_eq!(
16 | observer
17 | .persistent_state
18 | .gateway_identities
19 | .iter()
20 | .collect::>(),
21 | vec![]
22 | );
23 | assert_eq!(
24 | observer
25 | .persistent_state
26 | .gateway_versions
27 | .iter()
28 | .collect::>(),
29 | vec![]
30 | );
31 | }
32 |
33 | // Finish the sequence
34 | rx.extend_from_slice(right);
35 | let observer = rx.sink().sink().sink();
36 | assert!(observer.enumeration_state.is_none());
37 | assert_eq!(
38 | observer
39 | .persistent_state
40 | .gateway_identities
41 | .iter()
42 | .collect::>(),
43 | vec![
44 | (
45 | &GatewayID::try_from(0x1201).unwrap(),
46 | &LongAddress([0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16])
47 | ),
48 | (
49 | &GatewayID::try_from(0x1202).unwrap(),
50 | &LongAddress([0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16])
51 | ),
52 | ]
53 | );
54 | assert_eq!(
55 | observer
56 | .persistent_state
57 | .gateway_versions
58 | .iter()
59 | .collect::>(),
60 | vec![(
61 | &GatewayID::try_from(0x1201).unwrap(),
62 | &String::from("Mgate Version G8.59\rJul 6 2020\r16:51:51\rGW-H158.4.3S0.12\r")
63 | ),]
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/pv.rs:
--------------------------------------------------------------------------------
1 | //! An implementation of the gateway network.
2 | //!
3 | //! The gateway network consists of three layers, implemented in their own modules:
4 | //!
5 | //! * [`physical`]
6 | //! * [`link`]
7 | //! * [`network`]
8 | //! * [`application`]
9 |
10 | pub mod application;
11 | pub mod link;
12 | pub mod network;
13 | pub mod physical;
14 |
15 | pub use application::PacketType;
16 | pub use link::{LongAddress, ShortAddress, SlotCounter};
17 | pub use network::NodeID;
18 |
--------------------------------------------------------------------------------
/src/pv/application.rs:
--------------------------------------------------------------------------------
1 | use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
2 |
3 | mod receiver;
4 | pub use receiver::{Counters, Receiver, Sink};
5 |
6 | mod packet_type;
7 | pub use packet_type::PacketType;
8 |
9 | mod node_table;
10 | pub use node_table::{NodeTableRequest, NodeTableResponse, NodeTableResponseEntry};
11 | mod power_report;
12 | pub use power_report::{PowerReport, U12Pair};
13 | mod topology_report;
14 | pub use topology_report::TopologyReport;
15 |
--------------------------------------------------------------------------------
/src/pv/application/node_table.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 | use crate::pv::network::NodeAddress;
3 | use crate::pv::LongAddress;
4 | use zerocopy::big_endian;
5 |
6 | #[derive(
7 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned,
8 | )]
9 | #[repr(C)]
10 | pub struct NodeTableRequest {
11 | pub start_at: NodeAddress,
12 | }
13 |
14 | #[derive(Debug, FromBytes, Immutable, KnownLayout, Unaligned)]
15 | #[repr(C)]
16 | pub struct NodeTableResponse {
17 | pub entries_count: big_endian::U16,
18 | pub entries: [NodeTableResponseEntry],
19 | }
20 |
21 | #[derive(
22 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned,
23 | )]
24 | #[repr(C)]
25 | pub struct NodeTableResponseEntry {
26 | pub long_address: LongAddress,
27 | pub node_id: NodeAddress,
28 | }
29 |
30 | #[cfg(test)]
31 | mod tests {
32 | use super::*;
33 |
34 | #[test]
35 | fn request() {
36 | assert_eq!(
37 | NodeTableRequest::ref_from_bytes(b"\x00\x02"),
38 | Ok(&NodeTableRequest { start_at: 2.into() })
39 | );
40 | }
41 |
42 | #[test]
43 | fn response() {
44 | let response = NodeTableResponse::ref_from_bytes(b"\x00\x00").unwrap();
45 | assert_eq!(response.entries_count.get(), 0);
46 | assert_eq!(response.entries.len(), 0);
47 |
48 | let response = NodeTableResponse::ref_from_bytes(
49 | b"\x00\x0C\x04\xC0\x5B\x40\x00\xA2\x34\x6F\x00\x02\x04\xC0\x5B\x40\x00\xA2\x34\x71\x00\x03",
50 | ).unwrap();
51 | assert_eq!(response.entries_count.get(), 0x000c);
52 | assert_eq!(response.entries.len(), 2);
53 | assert_eq!(
54 | response.entries[0].long_address,
55 | LongAddress([0x04, 0xC0, 0x5B, 0x40, 0x00, 0xA2, 0x34, 0x6F])
56 | );
57 | assert_eq!(response.entries[0].node_id, 0x0002.into());
58 | assert_eq!(
59 | response.entries[1].long_address,
60 | LongAddress([0x04, 0xC0, 0x5B, 0x40, 0x00, 0xA2, 0x34, 0x71])
61 | );
62 | assert_eq!(response.entries[1].node_id, 0x0003.into());
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/pv/application/packet_type.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | #[derive(Copy, Clone, Eq, PartialEq, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable)]
4 | #[repr(transparent)]
5 | pub struct PacketType(pub u8);
6 | impl PacketType {
7 | pub const STRING_REQUEST: Self = Self(0x06);
8 | pub const STRING_RESPONSE: Self = Self(0x07);
9 | pub const TOPOLOGY_REPORT: Self = Self(0x09);
10 | pub const GATEWAY_RADIO_CONFIGURATION_REQUEST: Self = Self(0x0D);
11 | pub const GATEWAY_RADIO_CONFIGURATION_RESPONSE: Self = Self(0x0E);
12 | pub const PV_CONFIGURATION_REQUEST: Self = Self(0x13);
13 | pub const PV_CONFIGURATION_RESPONSE: Self = Self(0x18);
14 | pub const BROADCAST: Self = Self(0x22);
15 | pub const BROADCAST_ACK: Self = Self(0x23);
16 | pub const NODE_TABLE_REQUEST: Self = Self(0x26);
17 | pub const NODE_TABLE_RESPONSE: Self = Self(0x27);
18 | pub const LONG_NETWORK_STATUS_REQUEST: Self = Self(0x2D);
19 | pub const NETWORK_STATUS_REQUEST: Self = Self(0x2E);
20 | pub const NETWORK_STATUS_RESPONSE: Self = Self(0x2F);
21 | pub const POWER_REPORT: Self = Self(0x31);
22 | }
23 |
24 | impl std::fmt::Debug for PacketType {
25 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
26 | match *self {
27 | PacketType::STRING_REQUEST => f.write_str("PacketType::STRING_REQUEST"),
28 | PacketType::STRING_RESPONSE => f.write_str("PacketType::STRING_RESPONSE"),
29 | PacketType::TOPOLOGY_REPORT => f.write_str("PacketType::TOPOLOGY_REPORT"),
30 | PacketType::GATEWAY_RADIO_CONFIGURATION_REQUEST => {
31 | f.write_str("PacketType::GATEWAY_RADIO_CONFIGURATION_REQUEST")
32 | }
33 | PacketType::GATEWAY_RADIO_CONFIGURATION_RESPONSE => {
34 | f.write_str("PacketType::GATEWAY_RADIO_CONFIGURATION_RESPONSE")
35 | }
36 | PacketType::PV_CONFIGURATION_REQUEST => {
37 | f.write_str("PacketType::PV_CONFIGURATION_REQUEST")
38 | }
39 | PacketType::PV_CONFIGURATION_RESPONSE => {
40 | f.write_str("PacketType::PV_CONFIGURATION_RESPONSE")
41 | }
42 | PacketType::BROADCAST => f.write_str("PacketType::BROADCAST"),
43 | PacketType::BROADCAST_ACK => f.write_str("PacketType::BROADCAST_ACK"),
44 | PacketType::NODE_TABLE_REQUEST => f.write_str("PacketType::NODE_TABLE_REQUEST"),
45 | PacketType::NODE_TABLE_RESPONSE => f.write_str("PacketType::NODE_TABLE_RESPONSE"),
46 | PacketType::LONG_NETWORK_STATUS_REQUEST => {
47 | f.write_str("PacketType::LONG_NETWORK_STATUS_REQUEST")
48 | }
49 | PacketType::NETWORK_STATUS_REQUEST => f.write_str("PacketType::NETWORK_STATUS_REQUEST"),
50 | PacketType::NETWORK_STATUS_RESPONSE => {
51 | f.write_str("PacketType::NETWORK_STATUS_RESPONSE")
52 | }
53 | PacketType::POWER_REPORT => f.write_str("PacketType::POWER_REPORT"),
54 | _ => f
55 | .debug_tuple("PacketType")
56 | .field(&format_args!("{:#04X}", self.0))
57 | .finish(),
58 | }
59 | }
60 | }
61 |
62 | impl std::fmt::Display for PacketType {
63 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
64 | match *self {
65 | PacketType::STRING_REQUEST => f.write_str("STRING_REQUEST"),
66 | PacketType::STRING_RESPONSE => f.write_str("STRING_RESPONSE"),
67 | PacketType::TOPOLOGY_REPORT => f.write_str("TOPOLOGY_REPORT"),
68 | PacketType::GATEWAY_RADIO_CONFIGURATION_REQUEST => {
69 | f.write_str("GATEWAY_RADIO_CONFIGURATION_REQUEST")
70 | }
71 | PacketType::GATEWAY_RADIO_CONFIGURATION_RESPONSE => {
72 | f.write_str("GATEWAY_RADIO_CONFIGURATION_RESPONSE")
73 | }
74 | PacketType::PV_CONFIGURATION_REQUEST => f.write_str("PV_CONFIGURATION_REQUEST"),
75 | PacketType::PV_CONFIGURATION_RESPONSE => f.write_str("PV_CONFIGURATION_RESPONSE"),
76 | PacketType::BROADCAST => f.write_str("BROADCAST"),
77 | PacketType::BROADCAST_ACK => f.write_str("BROADCAST_ACK"),
78 | PacketType::NODE_TABLE_REQUEST => f.write_str("NODE_TABLE_REQUEST"),
79 | PacketType::NODE_TABLE_RESPONSE => f.write_str("NODE_TABLE_RESPONSE"),
80 | PacketType::LONG_NETWORK_STATUS_REQUEST => f.write_str("LONG_NETWORK_STATUS_REQUEST"),
81 | PacketType::NETWORK_STATUS_REQUEST => f.write_str("NETWORK_STATUS_REQUEST"),
82 | PacketType::NETWORK_STATUS_RESPONSE => f.write_str("NETWORK_STATUS_RESPONSE"),
83 | PacketType::POWER_REPORT => f.write_str("POWER_REPORT"),
84 | _ => write!(f, "{:#04X}", self.0),
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/pv/application/power_report.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 | use crate::pv::physical::RSSI;
3 | use crate::pv::SlotCounter;
4 |
5 | #[derive(
6 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned,
7 | )]
8 | #[repr(C)]
9 | pub struct PowerReport {
10 | pub voltage_in_and_voltage_out: U12Pair,
11 | pub dc_dc_duty_cycle: u8,
12 | pub current_and_temperature: U12Pair,
13 | pub unknown: [u8; 3],
14 | pub slot_counter: SlotCounter,
15 | pub rssi: RSSI,
16 | }
17 |
18 | /// A pair of 12-bit unsigned integers packed into a single `[u8; 3]`.
19 | #[derive(Copy, Clone, Eq, PartialEq, FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned)]
20 | #[repr(C)]
21 | pub struct U12Pair(pub [u8; 3]);
22 |
23 | impl std::fmt::Debug for U12Pair {
24 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
25 | let (a, b): (u16, u16) = (*self).into();
26 | f.debug_tuple("U12Pair")
27 | .field(&format_args!("{:#05x}", a))
28 | .field(&format_args!("{:#05x}", b))
29 | .finish()
30 | }
31 | }
32 |
33 | impl From for (u16, u16) {
34 | fn from(value: U12Pair) -> Self {
35 | (
36 | u16::from_be_bytes([value.0[0], value.0[1]]) >> 4,
37 | u16::from_be_bytes([value.0[1], value.0[2]]) & 0x0fff,
38 | )
39 | }
40 | }
41 | impl TryFrom<(u16, u16)> for U12Pair {
42 | type Error = ();
43 |
44 | fn try_from((a, b): (u16, u16)) -> Result {
45 | if a & 0xfff != a || b & 0xfff != b {
46 | Err(())
47 | } else {
48 | let a = (a << 4).to_be_bytes();
49 | let b = b.to_be_bytes();
50 | Ok(Self([a[0], a[1] | b[0], b[1]]))
51 | }
52 | }
53 | }
54 |
55 | #[cfg(test)]
56 | mod tests {
57 | use super::*;
58 |
59 | #[test]
60 | fn u12_pair() {
61 | let pair = U12Pair([0x2b, 0x61, 0x58]);
62 | assert_eq!(<(u16, u16)>::from(pair), (0x2b6, 0x158));
63 | assert_eq!(::try_from((0x2b6, 0x158)), Ok(pair));
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/pv/application/receiver.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 | use crate::gateway::GatewayID;
3 | use crate::pv::network::{NodeAddress, ReceivedPacketHeader};
4 | use crate::pv::{LongAddress, NodeID, PacketType, SlotCounter};
5 | use crate::{gateway, pv};
6 |
7 | pub trait Sink {
8 | fn string_request(&mut self, gateway_id: GatewayID, pv_node_id: pv::NodeID, request: &str);
9 | fn string_response(&mut self, gateway_id: GatewayID, pv_node_id: pv::NodeID, response: &str);
10 | fn node_table_page(
11 | &mut self,
12 | gateway_id: GatewayID,
13 | start_address: NodeAddress,
14 | nodes: &[NodeTableResponseEntry],
15 | );
16 |
17 | fn topology_report(
18 | &mut self,
19 | gateway_id: GatewayID,
20 | pv_node_id: pv::NodeID,
21 | topology_report: &TopologyReport,
22 | );
23 | fn power_report(
24 | &mut self,
25 | gateway_id: GatewayID,
26 | pv_node_id: pv::NodeID,
27 | power_report: &PowerReport,
28 | );
29 | }
30 |
31 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
32 | pub struct Counters {
33 | invalid_received_packet_node_ids: u64,
34 | invalid_power_reports: u64,
35 | power_reports: u64,
36 | invalid_topology_reports: u64,
37 | topology_reports: u64,
38 | invalid_node_table_requests: u64,
39 | invalid_node_table_responses: u64,
40 | invalid_string_commands: u64,
41 | string_commands: u64,
42 | invalid_string_responses: u64,
43 | string_responses: u64,
44 | }
45 |
46 | #[derive(Debug)]
47 | pub struct Receiver {
48 | sink: S,
49 | counters: Counters,
50 | }
51 |
52 | impl Receiver {
53 | pub fn new(sink: S) -> Self {
54 | Self {
55 | sink,
56 | counters: Default::default(),
57 | }
58 | }
59 |
60 | pub fn sink(&self) -> &S {
61 | &self.sink
62 | }
63 |
64 | pub fn sink_mut(&mut self) -> &mut S {
65 | &mut self.sink
66 | }
67 |
68 | pub fn into_inner(self) -> S {
69 | self.sink
70 | }
71 |
72 | pub fn counters(&self) -> &Counters {
73 | &self.counters
74 | }
75 |
76 | fn node_table_command(&mut self, gateway_id: GatewayID, request: &[u8], response: &[u8]) {
77 | let Ok(request) = NodeTableRequest::ref_from_bytes(request) else {
78 | self.counters.invalid_node_table_requests += 1;
79 | return;
80 | };
81 |
82 | let Ok(response) = NodeTableResponse::ref_from_bytes(response) else {
83 | self.counters.invalid_node_table_responses += 1;
84 | return;
85 | };
86 |
87 | if response.entries.len() != response.entries_count.get() as usize {
88 | self.counters.invalid_node_table_responses += 1;
89 | return;
90 | };
91 |
92 | self.sink
93 | .node_table_page(gateway_id, request.start_at, &response.entries);
94 | }
95 |
96 | fn string_command(&mut self, gateway_id: GatewayID, request: &[u8], response: &[u8]) {
97 | let Ok((node, request)) = NodeAddress::ref_from_prefix(request) else {
98 | self.counters.invalid_string_commands += 1;
99 | return;
100 | };
101 | let Ok(node) = NodeID::try_from(*node) else {
102 | self.counters.invalid_string_commands += 1;
103 | return;
104 | };
105 |
106 | let Ok(request) = std::str::from_utf8(request) else {
107 | self.counters.invalid_string_commands += 1;
108 | return;
109 | };
110 |
111 | if !response.is_empty() {
112 | self.counters.invalid_string_commands += 1;
113 | return;
114 | }
115 |
116 | self.counters.string_commands += 1;
117 |
118 | self.sink.string_request(gateway_id, node, request);
119 | }
120 | }
121 |
122 | impl gateway::transport::Sink for Receiver {
123 | fn enumeration_started(&mut self, enumeration_gateway_id: GatewayID) {
124 | self.sink.enumeration_started(enumeration_gateway_id)
125 | }
126 |
127 | fn gateway_identity_observed(&mut self, gateway_id: GatewayID, address: LongAddress) {
128 | self.sink.gateway_identity_observed(gateway_id, address)
129 | }
130 |
131 | fn gateway_version_observed(&mut self, gateway_id: GatewayID, version: &str) {
132 | self.sink.gateway_version_observed(gateway_id, version)
133 | }
134 |
135 | fn enumeration_ended(&mut self, gateway_id: GatewayID) {
136 | self.sink.enumeration_ended(gateway_id)
137 | }
138 |
139 | fn gateway_slot_counter_captured(&mut self, gateway_id: GatewayID) {
140 | self.sink.gateway_slot_counter_captured(gateway_id)
141 | }
142 |
143 | fn gateway_slot_counter_observed(&mut self, gateway_id: GatewayID, slot_counter: SlotCounter) {
144 | self.sink
145 | .gateway_slot_counter_observed(gateway_id, slot_counter)
146 | }
147 |
148 | fn packet_received(
149 | &mut self,
150 | gateway_id: GatewayID,
151 | header: &ReceivedPacketHeader,
152 | data: &[u8],
153 | ) {
154 | self.sink.packet_received(gateway_id, header, data);
155 |
156 | let Ok(node_id) = pv::NodeID::try_from(header.node_address) else {
157 | self.counters.invalid_received_packet_node_ids += 1;
158 | return;
159 | };
160 |
161 | match header.packet_type {
162 | PacketType::STRING_RESPONSE => {
163 | if let Ok(response) = std::str::from_utf8(data) {
164 | self.counters.string_responses += 1;
165 | self.sink.string_response(gateway_id, node_id, response);
166 | } else {
167 | self.counters.invalid_string_responses += 1;
168 | }
169 | }
170 | PacketType::TOPOLOGY_REPORT => {
171 | if let Ok(topology_report) = TopologyReport::ref_from_bytes(data) {
172 | self.counters.topology_reports += 1;
173 | self.sink
174 | .topology_report(gateway_id, node_id, topology_report);
175 | } else {
176 | self.counters.invalid_topology_reports += 1;
177 | }
178 | }
179 | PacketType::POWER_REPORT => {
180 | if let Ok(power_report) = PowerReport::ref_from_bytes(data) {
181 | self.counters.power_reports += 1;
182 | self.sink.power_report(gateway_id, node_id, power_report);
183 | } else {
184 | self.counters.invalid_power_reports += 1;
185 | }
186 | }
187 | _ => {}
188 | }
189 | }
190 |
191 | fn command_executed(
192 | &mut self,
193 | gateway_id: GatewayID,
194 | request: (PacketType, &[u8]),
195 | response: (PacketType, &[u8]),
196 | ) {
197 | self.sink.command_executed(gateway_id, request, response);
198 |
199 | match (request.0, response.0) {
200 | (PacketType::NODE_TABLE_REQUEST, PacketType::NODE_TABLE_RESPONSE) => {
201 | self.node_table_command(gateway_id, request.1, response.1);
202 | }
203 |
204 | (PacketType::STRING_REQUEST, PacketType::STRING_RESPONSE) => {
205 | self.string_command(gateway_id, request.1, response.1);
206 | }
207 | //(PacketType::BROADCAST, PacketType::BROADCAST_ACK) => {}
208 | (
209 | PacketType::NETWORK_STATUS_REQUEST | PacketType::LONG_NETWORK_STATUS_REQUEST,
210 | PacketType::NETWORK_STATUS_RESPONSE,
211 | ) => {
212 | // TODO
213 | }
214 | _ => {
215 | /*
216 | eprintln!(
217 | "unhandled command: {} ({} bytes) => {} ({} bytes)",
218 | request.0,
219 | request.1.len(),
220 | response.0,
221 | response.1.len()
222 | );
223 | */
224 | }
225 | }
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/pv/application/string.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 | use std::convert::TryFrom;
3 |
4 | #[derive(
5 | Debug, Eq, PartialEq
6 | )]
7 | #[repr(C)]
8 | pub struct StringRequest {
9 | pub pv_node_id: pv::network::NodeID,
10 | pub request: [u8],
11 | }
12 | impl Message for StringRequest {
13 | const PACKET_TYPE: PacketType = PacketType::STRING_REQUEST;
14 | }
15 |
16 | impl TryFrom<&StringRequest> for &str {
17 | type Error = std::str::Utf8Error;
18 |
19 | fn try_from(value: &StringRequest) -> Result {
20 | std::str::from_utf8(&value.request)
21 | }
22 | }
23 |
24 | impl From<&StringRequest> for String {
25 | fn from(value: &StringRequest) -> Self {
26 | String::from_utf8_lossy(&value.request).into()
27 | }
28 | }
29 | impl std::fmt::Display for StringRequest {
30 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
31 | f.write_str(&String::from_utf8_lossy(&self.request))
32 | }
33 | }
34 |
35 | #[derive(
36 | Debug, Eq, PartialEq, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable,
37 | )]
38 | #[repr(C)]
39 | pub struct StringResponse {
40 | pub response: [u8],
41 | }
42 | impl Message for StringResponse {
43 | const PACKET_TYPE: PacketType = PacketType::STRING_RESPONSE;
44 | }
45 |
46 | impl TryFrom<&StringResponse> for &str {
47 | type Error = std::str::Utf8Error;
48 |
49 | fn try_from(value: &StringResponse) -> Result {
50 | std::str::from_utf8(&value.response)
51 | }
52 | }
53 |
54 | impl From<&StringResponse> for String {
55 | fn from(value: &StringRequest) -> Self {
56 | String::from_utf8_lossy(&value.request).into()
57 | }
58 | }
59 | impl std::fmt::Display for StringResponse {
60 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
61 | f.write_str(&String::from_utf8_lossy(&self.response))
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/pv/application/topology_report.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 | use crate::pv::network::NodeAddress;
3 | use crate::pv::physical::RSSI;
4 | use crate::pv::{LongAddress, ShortAddress};
5 |
6 | #[derive(
7 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned,
8 | )]
9 | #[repr(C)]
10 | pub struct TopologyReport {
11 | pub short_address: ShortAddress,
12 | pub pv_node_id: NodeAddress,
13 | pub next_hop: NodeAddress,
14 | pub unknown_1: [u8; 2],
15 | pub long_address: LongAddress,
16 | pub rssi: RSSI,
17 | pub unknown_2: [u8; 5],
18 | }
19 |
--------------------------------------------------------------------------------
/src/pv/link.rs:
--------------------------------------------------------------------------------
1 | use schemars::JsonSchema;
2 | use serde::{Deserialize, Deserializer, Serialize, Serializer};
3 | use zerocopy::{big_endian, FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned, U16};
4 |
5 | mod slot_counter;
6 | pub use slot_counter::{InvalidSlotNumber, SlotCounter, SlotEpoch, SlotNumber};
7 |
8 | /// A 16-bit PV link layer (802.15.4) short address.
9 | #[derive(
10 | Copy,
11 | Clone,
12 | Eq,
13 | PartialEq,
14 | Ord,
15 | PartialOrd,
16 | FromBytes,
17 | IntoBytes,
18 | Unaligned,
19 | KnownLayout,
20 | Immutable,
21 | )]
22 | #[repr(transparent)]
23 | pub struct ShortAddress(pub big_endian::U16);
24 |
25 | impl std::fmt::Debug for ShortAddress {
26 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
27 | f.debug_tuple("ShortAddress")
28 | .field(&format_args!("{:#06X}", u16::from(self.0)))
29 | .finish()
30 | }
31 | }
32 |
33 | impl std::fmt::Display for ShortAddress {
34 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
35 | write!(f, "{:#06X}", u16::from(self.0))
36 | }
37 | }
38 |
39 | /// A 64-bit PV link layer (802.15.4) long address.
40 | #[derive(
41 | Copy,
42 | Clone,
43 | Eq,
44 | PartialEq,
45 | Ord,
46 | PartialOrd,
47 | Serialize,
48 | Deserialize,
49 | JsonSchema,
50 | FromBytes,
51 | IntoBytes,
52 | Unaligned,
53 | KnownLayout,
54 | Immutable,
55 | )]
56 | #[repr(transparent)]
57 | pub struct LongAddress(pub [u8; 8]);
58 |
59 | impl LongAddress {
60 | pub fn barcode(&self) -> crate::barcode::Barcode {
61 | self.into()
62 | }
63 | }
64 |
65 | impl std::fmt::Debug for LongAddress {
66 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
67 | f.debug_tuple("LongAddress")
68 | .field(&format_args!(
69 | "[{:#04X}, {:#04X}, {:#04X}, {:#04X}, {:#04X}, {:#04X}, {:#04X}, {:#04X}]",
70 | self.0[0],
71 | self.0[1],
72 | self.0[2],
73 | self.0[3],
74 | self.0[4],
75 | self.0[5],
76 | self.0[6],
77 | self.0[7],
78 | ))
79 | .finish()
80 | }
81 | }
82 |
83 | impl std::fmt::Display for LongAddress {
84 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
85 | write!(
86 | f,
87 | "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
88 | self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7],
89 | )
90 | }
91 | }
92 |
93 | #[derive(Copy, Clone, Eq, PartialEq, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable)]
94 | #[repr(transparent)]
95 | pub struct DSN(pub u8);
96 |
97 | impl std::ops::Add for DSN {
98 | type Output = Self;
99 |
100 | fn add(self, rhs: u8) -> Self::Output {
101 | Self(self.0.wrapping_add(rhs))
102 | }
103 | }
104 |
105 | impl std::fmt::Debug for DSN {
106 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
107 | f.debug_tuple("DSN")
108 | .field(&format_args!("{:#04X}", self.0))
109 | .finish()
110 | }
111 | }
112 |
113 | impl std::fmt::Display for DSN {
114 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
115 | write!(f, "{:#04X}", self.0)
116 | }
117 | }
118 |
119 | #[cfg(test)]
120 | mod tests {
121 | use super::*;
122 |
123 | #[test]
124 | fn long_address_fmt() {
125 | assert_eq!(
126 | format!(
127 | "{:?}",
128 | &LongAddress([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])
129 | ),
130 | "LongAddress([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])"
131 | );
132 | assert_eq!(
133 | format!(
134 | "{}",
135 | &LongAddress([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])
136 | ),
137 | "01:02:03:04:05:06:07:08"
138 | );
139 | }
140 |
141 | #[test]
142 | fn short_address_fmt() {
143 | assert_eq!(
144 | format!("{:?}", &ShortAddress(0x1234.into())),
145 | "ShortAddress(0x1234)",
146 | );
147 | assert_eq!(format!("{}", &ShortAddress(0x1234.into())), "0x1234",);
148 | }
149 |
150 | #[test]
151 | fn dsn_fmt() {
152 | assert_eq!(format!("{:?}", &DSN(0x12)), "DSN(0x12)",);
153 | assert_eq!(format!("{}", &DSN(0x12)), "0x12",);
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/pv/link/slot_counter.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 | use std::convert::Into;
3 |
4 | /// A slot counter.
5 | ///
6 | /// Slot counters are logically divided into two components: an epoch and a slot number. Each epoch
7 | /// takes 60 ± 1% seconds, so the slot counter repeats after 4 minutes.
8 | #[derive(
9 | Copy,
10 | Clone,
11 | Eq,
12 | PartialEq,
13 | Ord,
14 | PartialOrd,
15 | FromBytes,
16 | IntoBytes,
17 | Unaligned,
18 | KnownLayout,
19 | Immutable,
20 | )]
21 | #[repr(transparent)]
22 | pub struct SlotCounter(pub big_endian::U16);
23 |
24 | impl SlotCounter {
25 | pub const ZERO: Self = SlotCounter(U16::ZERO);
26 |
27 | pub fn new(epoch: SlotEpoch, slot_number: SlotNumber) -> Self {
28 | let value: u16 = match epoch {
29 | SlotEpoch::Epoch0 => 0x0000,
30 | SlotEpoch::Epoch4 => 0x4000,
31 | SlotEpoch::Epoch8 => 0x8000,
32 | SlotEpoch::EpochC => 0xC000,
33 | } | slot_number.0;
34 |
35 | Self(value.into())
36 | }
37 |
38 | pub fn epoch(&self) -> SlotEpoch {
39 | match self.0.get() & 0xc000 {
40 | 0x0000 => SlotEpoch::Epoch0,
41 | 0x4000 => SlotEpoch::Epoch4,
42 | 0x8000 => SlotEpoch::Epoch8,
43 | 0xC000 => SlotEpoch::EpochC,
44 | _ => unreachable!(),
45 | }
46 | }
47 |
48 | pub fn slot_number(&self) -> Result {
49 | (self.0.get() & 0x3fff).try_into()
50 | }
51 |
52 | pub fn slots_since(&self, past: &Self) -> Result {
53 | let self_abs_slots = self.epoch() as u8 as u16 * 12000 + self.slot_number()?.0;
54 | let past_abs_slots = past.epoch() as u8 as u16 * 12000 + past.slot_number()?.0;
55 |
56 | Ok(if self_abs_slots > past_abs_slots {
57 | // Expected
58 | self_abs_slots - past_abs_slots
59 | } else {
60 | // We wrapped
61 | 48000 - past_abs_slots + self_abs_slots
62 | })
63 | }
64 | }
65 |
66 | impl From for SlotCounter {
67 | fn from(value: u16) -> Self {
68 | Self(value.into())
69 | }
70 | }
71 | impl From for u16 {
72 | fn from(value: SlotCounter) -> Self {
73 | value.0.get()
74 | }
75 | }
76 |
77 | impl std::fmt::Debug for SlotCounter {
78 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
79 | match self.slot_number() {
80 | Ok(n) => f
81 | .debug_tuple("SlotCounter")
82 | .field(&self.epoch())
83 | .field(&n)
84 | .finish(),
85 | other => f
86 | .debug_tuple("SlotCounter")
87 | .field(&self.epoch())
88 | .field(&other)
89 | .finish(),
90 | }
91 | }
92 | }
93 |
94 | impl Serialize for SlotCounter {
95 | fn serialize(&self, serializer: S) -> Result
96 | where
97 | S: Serializer,
98 | {
99 | serializer.serialize_u16(self.0.get())
100 | }
101 | }
102 |
103 | impl<'de> Deserialize<'de> for SlotCounter {
104 | fn deserialize(deserializer: D) -> Result
105 | where
106 | D: Deserializer<'de>,
107 | {
108 | u16::deserialize(deserializer).map(U16::from).map(Self)
109 | }
110 | }
111 |
112 | /// An epoch of slot numbers, corresponding to approximately one minute of real time.
113 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
114 | #[repr(u8)]
115 | pub enum SlotEpoch {
116 | Epoch0 = 0,
117 | Epoch4 = 1,
118 | Epoch8 = 2,
119 | EpochC = 3,
120 | }
121 |
122 | impl std::ops::Add for SlotEpoch {
123 | type Output = SlotEpoch;
124 |
125 | // SlotEpoch addition wraps
126 | #[allow(clippy::suspicious_arithmetic_impl)]
127 | fn add(self, rhs: u8) -> Self::Output {
128 | match (self as u8).wrapping_add(rhs) % 4 {
129 | 0 => Self::Epoch0,
130 | 1 => Self::Epoch4,
131 | 2 => Self::Epoch8,
132 | 3 => Self::EpochC,
133 | _ => unreachable!(),
134 | }
135 | }
136 | }
137 |
138 | impl std::ops::AddAssign for SlotEpoch {
139 | fn add_assign(&mut self, rhs: u8) {
140 | *self = *self + rhs;
141 | }
142 | }
143 |
144 | /// A slot number.
145 | ///
146 | /// Each slot takes 5 ± 1% milliseconds. Slot numbers range from 0 to 11999 inclusive.
147 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
148 | pub struct SlotNumber(u16);
149 |
150 | impl From for u16 {
151 | fn from(value: SlotNumber) -> Self {
152 | value.0
153 | }
154 | }
155 |
156 | impl TryFrom for SlotNumber {
157 | type Error = InvalidSlotNumber;
158 |
159 | fn try_from(value: u16) -> Result {
160 | if value <= 0x2edf {
161 | Ok(SlotNumber(value))
162 | } else {
163 | Err(InvalidSlotNumber(value))
164 | }
165 | }
166 | }
167 |
168 | #[derive(thiserror::Error, Debug, Copy, Clone, Eq, PartialEq)]
169 | #[error("invalid slot number: {0:#06x}")]
170 | pub struct InvalidSlotNumber(pub u16);
171 |
172 | #[cfg(test)]
173 | mod tests {
174 | use super::*;
175 |
176 | #[test]
177 | fn roundtrip() {
178 | let slot = SlotCounter(0x0000.into());
179 | assert_eq!(slot.epoch(), SlotEpoch::Epoch0);
180 | assert_eq!(slot.slot_number(), Ok(SlotNumber(0)));
181 | assert_eq!(
182 | SlotCounter::new(SlotEpoch::Epoch0, SlotNumber::try_from(0).unwrap()),
183 | slot
184 | );
185 |
186 | let slot = SlotCounter(0x2edf.into());
187 | assert_eq!(slot.epoch(), SlotEpoch::Epoch0);
188 | assert_eq!(slot.slot_number(), Ok(SlotNumber(11999)));
189 | assert_eq!(
190 | SlotCounter::new(SlotEpoch::Epoch0, SlotNumber::try_from(11999).unwrap()),
191 | slot
192 | );
193 |
194 | let slot = SlotCounter(0x2ee0.into());
195 | assert_eq!(slot.epoch(), SlotEpoch::Epoch0);
196 | assert_eq!(slot.slot_number(), Err(InvalidSlotNumber(0x2ee0)));
197 | }
198 |
199 | #[test]
200 | fn slots_since() {
201 | assert_eq!(
202 | SlotCounter(0x0000.into()).slots_since(&SlotCounter(0xeedf.into())),
203 | Ok(1)
204 | );
205 | assert_eq!(
206 | SlotCounter(0x4000.into()).slots_since(&SlotCounter(0xeedf.into())),
207 | Ok(12001)
208 | );
209 | assert_eq!(
210 | SlotCounter(0x8000.into()).slots_since(&SlotCounter(0xeedf.into())),
211 | Ok(24001)
212 | );
213 | assert_eq!(
214 | SlotCounter(0xc000.into()).slots_since(&SlotCounter(0xeedf.into())),
215 | Ok(36001)
216 | );
217 |
218 | assert_eq!(
219 | SlotCounter(0xeedf.into()).slots_since(&SlotCounter(0xc000.into())),
220 | Ok(11999)
221 | );
222 | assert_eq!(
223 | SlotCounter(0xeedf.into()).slots_since(&SlotCounter(0x8000.into())),
224 | Ok(23999)
225 | );
226 | assert_eq!(
227 | SlotCounter(0xeedf.into()).slots_since(&SlotCounter(0x4000.into())),
228 | Ok(35999)
229 | );
230 | assert_eq!(
231 | SlotCounter(0xeedf.into()).slots_since(&SlotCounter(0x0000.into())),
232 | Ok(47999)
233 | );
234 |
235 | assert_eq!(
236 | SlotCounter(0x6edf.into()).slots_since(&SlotCounter(0x4000.into())),
237 | Ok(11999)
238 | );
239 | assert_eq!(
240 | SlotCounter(0x6edf.into()).slots_since(&SlotCounter(0x0000.into())),
241 | Ok(23999)
242 | );
243 | assert_eq!(
244 | SlotCounter(0x6edf.into()).slots_since(&SlotCounter(0xc000.into())),
245 | Ok(35999)
246 | );
247 | assert_eq!(
248 | SlotCounter(0x6edf.into()).slots_since(&SlotCounter(0x8000.into())),
249 | Ok(47999)
250 | );
251 |
252 | assert_eq!(
253 | SlotCounter(0x0100.into()).slots_since(&SlotCounter(0x0080.into())),
254 | Ok(128)
255 | );
256 | assert_eq!(
257 | SlotCounter(0x0100.into()).slots_since(&SlotCounter(0xc080.into())),
258 | Ok(12128)
259 | );
260 | assert_eq!(
261 | SlotCounter(0x0100.into()).slots_since(&SlotCounter(0x8080.into())),
262 | Ok(24128)
263 | );
264 | assert_eq!(
265 | SlotCounter(0x0100.into()).slots_since(&SlotCounter(0x4080.into())),
266 | Ok(36128)
267 | );
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/src/pv/network.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 | use schemars::JsonSchema;
3 | use serde::{Deserialize, Serialize};
4 | use std::mem::size_of;
5 | use std::num::{NonZeroU16, TryFromIntError};
6 | use zerocopy::{big_endian, FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
7 |
8 | /// A 16-bit PV network layer node ID.
9 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema)]
10 | #[repr(transparent)]
11 | pub struct NodeID(NonZeroU16);
12 | impl NodeID {
13 | pub const GATEWAY: Self = NodeID(NonZeroU16::MIN);
14 | pub const MAX: Self = NodeID(NonZeroU16::MAX);
15 |
16 | pub fn successor(&self) -> Option {
17 | self.0.checked_add(1).map(Self)
18 | }
19 | }
20 |
21 | impl std::fmt::Debug for NodeID {
22 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
23 | f.debug_tuple("NodeID")
24 | .field(&format_args!("{:#06X}", u16::from(self.0)))
25 | .finish()
26 | }
27 | }
28 |
29 | impl std::fmt::Display for NodeID {
30 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
31 | write!(f, "{:#06X}", u16::from(self.0))
32 | }
33 | }
34 | impl TryFrom for NodeID {
35 | type Error = TryFromIntError;
36 |
37 | fn try_from(value: u16) -> Result {
38 | NonZeroU16::try_from(value).map(Self)
39 | }
40 | }
41 |
42 | /// A 16-bit PV network layer node address, which could be either a `NodeID` or the broadcast
43 | /// address.
44 | #[derive(
45 | Copy,
46 | Clone,
47 | Eq,
48 | PartialEq,
49 | Ord,
50 | PartialOrd,
51 | FromBytes,
52 | IntoBytes,
53 | Unaligned,
54 | KnownLayout,
55 | Immutable,
56 | )]
57 | #[repr(transparent)]
58 | pub struct NodeAddress(pub big_endian::U16);
59 | impl NodeAddress {
60 | pub const ZERO: Self = Self(big_endian::U16::ZERO);
61 | pub const GATEWAY: Self = Self(big_endian::U16::new(1));
62 | }
63 |
64 | impl std::fmt::Debug for NodeAddress {
65 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
66 | if *self == Self::ZERO {
67 | f.write_str("NodeAddress::ZERO")
68 | } else if *self == Self::GATEWAY {
69 | f.write_str("NodeAddress::GATEWAY")
70 | } else {
71 | f.debug_tuple("NodeAddress")
72 | .field(&format_args!("{:#06X}", u16::from(self.0)))
73 | .finish()
74 | }
75 | }
76 | }
77 |
78 | impl From for Option {
79 | fn from(value: NodeAddress) -> Self {
80 | match value {
81 | NodeAddress::ZERO => None,
82 | NodeAddress(id) => {
83 | let id = u16::from(id);
84 | assert_ne!(id, 0); // would be BROADCAST
85 | Some(NodeID(id.try_into().unwrap()))
86 | }
87 | }
88 | }
89 | }
90 |
91 | impl From> for NodeAddress {
92 | fn from(value: Option) -> Self {
93 | match value {
94 | None => NodeAddress::ZERO,
95 | Some(NodeID(id)) => NodeAddress(u16::from(id).into()),
96 | }
97 | }
98 | }
99 | impl From for NodeAddress {
100 | fn from(value: NodeID) -> Self {
101 | NodeAddress(u16::from(value.0).into())
102 | }
103 | }
104 | impl TryFrom for NodeID {
105 | type Error = TryFromIntError;
106 |
107 | fn try_from(value: NodeAddress) -> Result {
108 | NonZeroU16::try_from(value.0.get()).map(Self)
109 | }
110 | }
111 | impl From for NodeAddress {
112 | fn from(value: u16) -> Self {
113 | Self(value.into())
114 | }
115 | }
116 |
117 | impl std::fmt::Display for NodeAddress {
118 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
119 | write!(f, "{:04X}", u16::from(self.0))
120 | }
121 | }
122 |
123 | #[derive(
124 | Debug, Copy, Clone, Eq, PartialEq, FromBytes, IntoBytes, Unaligned, KnownLayout, Immutable,
125 | )]
126 | #[repr(C)]
127 | pub struct ReceivedPacketHeader {
128 | pub packet_type: application::PacketType,
129 | pub node_address: NodeAddress,
130 | pub short_address: ShortAddress,
131 | pub dsn: link::DSN,
132 | pub data_length: u8,
133 | }
134 |
135 | #[derive(thiserror::Error, Debug)]
136 | #[error("packet too short")]
137 | pub struct PacketTooShortError;
138 |
139 | /// An `Iterator` over zero or more received packets.
140 | #[derive(Debug, Clone, Eq, PartialEq)]
141 | pub struct ReceivedPackets<'a>(pub &'a [u8]);
142 | impl<'a> Iterator for ReceivedPackets<'a> {
143 | type Item = Result<(&'a ReceivedPacketHeader, &'a [u8]), PacketTooShortError>;
144 |
145 | fn next(&mut self) -> Option {
146 | if self.0.is_empty() {
147 | return None;
148 | } else if self.0.len() < size_of::() {
149 | self.0 = &[];
150 | return Some(Err(PacketTooShortError));
151 | }
152 |
153 | let (header, rest) = self.0.split_at(size_of::());
154 | let header = ReceivedPacketHeader::ref_from_bytes(header).unwrap(); // infallible
155 |
156 | let data_length = header.data_length as usize;
157 | if rest.len() < data_length {
158 | self.0 = &[];
159 | return Some(Err(PacketTooShortError));
160 | }
161 | let (data, rest) = rest.split_at(data_length);
162 | self.0 = rest;
163 |
164 | Some(Ok((header, data)))
165 | }
166 | }
167 |
168 | #[cfg(test)]
169 | mod tests {
170 | use super::*;
171 |
172 | #[test]
173 | fn node_id() {
174 | assert_eq!(NodeID::GATEWAY, NodeID::try_from(1).unwrap());
175 | assert_eq!(NodeID::MAX, NodeID::try_from(65535).unwrap());
176 |
177 | assert_eq!(
178 | NodeID::GATEWAY.successor(),
179 | Some(NodeID(NonZeroU16::try_from(2).unwrap()))
180 | );
181 | assert_eq!(NodeID::MAX.successor(), None);
182 |
183 | assert_eq!(NodeID::try_from(NodeAddress::GATEWAY), Ok(NodeID::GATEWAY));
184 | assert!(NodeID::try_from(NodeAddress(0.into())).is_err());
185 |
186 | assert_eq!(format!("{:?}", &NodeID::GATEWAY), "NodeID(0x0001)");
187 | assert_eq!(format!("{}", &NodeID::GATEWAY), "0x0001");
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/src/pv/physical.rs:
--------------------------------------------------------------------------------
1 | use schemars::JsonSchema;
2 | use serde::{Deserialize, Serialize};
3 | use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
4 |
5 | #[derive(
6 | Debug,
7 | Copy,
8 | Clone,
9 | Eq,
10 | PartialEq,
11 | FromBytes,
12 | IntoBytes,
13 | Unaligned,
14 | KnownLayout,
15 | Immutable,
16 | Serialize,
17 | Deserialize,
18 | JsonSchema,
19 | )]
20 | #[repr(transparent)]
21 | #[serde(transparent)]
22 | pub struct RSSI(pub u8);
23 |
--------------------------------------------------------------------------------
/src/test_data.rs:
--------------------------------------------------------------------------------
1 | pub const ENUMERATION_SEQUENCE: &'static [u8] = &[
2 | 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x0B, 0x00, 0x01, 0xFE, 0x83, 0x7E, 0x08, 0xFF, 0x7E,
3 | 0x07, 0x92, 0x01, 0x0B, 0x01, 0x01, 0x73, 0x10, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x00,
4 | 0x00, 0x00, 0x14, 0x37, 0x7E, 0x01, 0x92, 0x66, 0x12, 0x35, 0x06, 0x1A, 0x7E, 0x08, 0xFF, 0x7E,
5 | 0x07, 0x80, 0x00, 0x00, 0x15, 0x17, 0xE0, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x00, 0x00,
6 | 0x00, 0x14, 0x37, 0x7E, 0x01, 0x92, 0x66, 0x12, 0x35, 0x06, 0x1A, 0x7E, 0x08, 0xFF, 0x7E, 0x07,
7 | 0x80, 0x00, 0x00, 0x15, 0x17, 0xE0, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x00, 0x00, 0x00,
8 | 0x14, 0x37, 0x7E, 0x01, 0x92, 0x66, 0x12, 0x35, 0x06, 0x1A, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x80,
9 | 0x00, 0x00, 0x15, 0x17, 0xE0, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x00, 0x00, 0x00, 0x14,
10 | 0x37, 0x7E, 0x01, 0x92, 0x66, 0x12, 0x35, 0x06, 0x1A, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x80, 0x00,
11 | 0x00, 0x15, 0x17, 0xE0, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x00, 0x00, 0x00, 0x14, 0x37,
12 | 0x7E, 0x01, 0x92, 0x66, 0x12, 0x35, 0x06, 0x1A, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x80, 0x00, 0x00,
13 | 0x15, 0x17, 0xE0, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x35, 0x00, 0x38, 0x5A, 0x72,
14 | 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x35, 0x00, 0x39, 0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE,
15 | 0x16, 0x12, 0x35, 0xA7, 0x83, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x35, 0x00, 0x3C,
16 | 0x37, 0x7E, 0x01, 0x92, 0x66, 0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16, 0x12, 0x01, 0x58,
17 | 0x0B, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x35, 0x00, 0x3D, 0x99, 0x08, 0x7E, 0x08, 0x00, 0xFF,
18 | 0xFF, 0x7E, 0x07, 0x12, 0x35, 0x00, 0x38, 0x5A, 0x72, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07,
19 | 0x12, 0x35, 0x00, 0x38, 0x5A, 0x72, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x35, 0x00,
20 | 0x38, 0x5A, 0x72, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x35, 0x00, 0x38, 0x5A, 0x72,
21 | 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x35, 0x00, 0x38, 0x5A, 0x72, 0x7E, 0x08, 0x00,
22 | 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x00, 0x3A, 0x87, 0xB4, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92,
23 | 0x01, 0x00, 0x3B, 0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16, 0x12, 0x01, 0xE6, 0xA6, 0x7E,
24 | 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x00, 0x3C, 0x37, 0x7E, 0x01, 0x92, 0x66, 0x04,
25 | 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16, 0x12, 0x02, 0xDC, 0x60, 0x7E, 0x08, 0xFF, 0x7E, 0x07,
26 | 0x92, 0x01, 0x00, 0x3D, 0x56, 0xED, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x02, 0x00,
27 | 0x3A, 0xE3, 0x5B, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x02, 0x00, 0x3B, 0x04, 0xC0, 0x5B, 0x30,
28 | 0x00, 0x02, 0xBE, 0x16, 0x12, 0x02, 0x8A, 0x9A, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x00,
29 | 0x00, 0x00, 0x10, 0x37, 0x7E, 0x01, 0x92, 0x66, 0xC3, 0x27, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x80,
30 | 0x00, 0x00, 0x11, 0x33, 0xA6, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x00, 0x3A,
31 | 0x87, 0xB4, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x00, 0x3B, 0x04, 0xC0, 0x5B, 0x30, 0x00,
32 | 0x02, 0xBE, 0x16, 0x12, 0x01, 0xE6, 0xA6, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01,
33 | 0x00, 0x0A, 0x04, 0x85, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x00, 0x0B, 0x4D, 0x67, 0x61,
34 | 0x74, 0x65, 0x20, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x47, 0x38, 0x2E, 0x35, 0x39,
35 | 0x0D, 0x4A, 0x75, 0x6C, 0x20, 0x20, 0x36, 0x20, 0x32, 0x30, 0x32, 0x30, 0x0D, 0x31, 0x36, 0x3A,
36 | 0x35, 0x31, 0x3A, 0x35, 0x31, 0x0D, 0x47, 0x57, 0x2D, 0x48, 0x31, 0x35, 0x38, 0x2E, 0x34, 0x2E,
37 | 0x33, 0x53, 0x30, 0x2E, 0x31, 0x32, 0x0D, 0x8A, 0xE2, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07,
38 | 0x12, 0x01, 0x0E, 0x02, 0x5C, 0x93, 0x7E, 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x00, 0x06, 0x06,
39 | 0x62, 0x7E, 0x08, 0x00, 0xFF, 0xFF, 0x7E, 0x07, 0x12, 0x01, 0x0B, 0x00, 0x01, 0xFE, 0x83, 0x7E,
40 | 0x08, 0xFF, 0x7E, 0x07, 0x92, 0x01, 0x0B, 0x01, 0x01, 0x73, 0x10, 0x7E, 0x08,
41 | ];
42 |
--------------------------------------------------------------------------------