├── .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 | --------------------------------------------------------------------------------