├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src ├── application.rs ├── application └── service.rs ├── encoding.rs ├── encoding └── parse.rs ├── lib.rs ├── main.rs ├── network.rs ├── transport.rs └── transport ├── bacnetip.rs └── bacnetsc.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: BACnet Rust 2 | 3 | on: [push] 4 | 5 | jobs: 6 | ci: 7 | strategy: 8 | matrix: 9 | rust: 10 | - stable 11 | - nightly 12 | os: 13 | - ubuntu-latest 14 | - macos-latest 15 | - windows-latest 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v1 22 | 23 | - name: Setup rust toolchain 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: ${{ matrix.rust }} 28 | override: true 29 | components: rustfmt, clippy 30 | 31 | - name: Run cargo release build 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: build 35 | args: --release 36 | 37 | - name: Run cargo test 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | 42 | - name: Run cargo fmt check 43 | uses: actions-rs/cargo@v1 44 | with: 45 | command: fmt 46 | args: --all -- --check 47 | 48 | #- name: Run cargo clippy checks 49 | # uses: actions-rs/cargo@v1 50 | # with: 51 | # command: clippy 52 | # args: -- -D warnings 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /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 = "async-channel" 7 | version = "1.8.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" 10 | dependencies = [ 11 | "concurrent-queue", 12 | "event-listener", 13 | "futures-core", 14 | ] 15 | 16 | [[package]] 17 | name = "async-executor" 18 | version = "1.5.0" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" 21 | dependencies = [ 22 | "async-lock", 23 | "async-task", 24 | "concurrent-queue", 25 | "fastrand", 26 | "futures-lite", 27 | "slab", 28 | ] 29 | 30 | [[package]] 31 | name = "async-global-executor" 32 | version = "2.3.1" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" 35 | dependencies = [ 36 | "async-channel", 37 | "async-executor", 38 | "async-io", 39 | "async-lock", 40 | "blocking", 41 | "futures-lite", 42 | "once_cell", 43 | ] 44 | 45 | [[package]] 46 | name = "async-io" 47 | version = "1.12.0" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" 50 | dependencies = [ 51 | "async-lock", 52 | "autocfg", 53 | "concurrent-queue", 54 | "futures-lite", 55 | "libc", 56 | "log", 57 | "parking", 58 | "polling", 59 | "slab", 60 | "socket2", 61 | "waker-fn", 62 | "windows-sys", 63 | ] 64 | 65 | [[package]] 66 | name = "async-lock" 67 | version = "2.6.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" 70 | dependencies = [ 71 | "event-listener", 72 | "futures-lite", 73 | ] 74 | 75 | [[package]] 76 | name = "async-std" 77 | version = "1.12.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" 80 | dependencies = [ 81 | "async-channel", 82 | "async-global-executor", 83 | "async-io", 84 | "async-lock", 85 | "crossbeam-utils", 86 | "futures-channel", 87 | "futures-core", 88 | "futures-io", 89 | "futures-lite", 90 | "gloo-timers", 91 | "kv-log-macro", 92 | "log", 93 | "memchr", 94 | "once_cell", 95 | "pin-project-lite", 96 | "pin-utils", 97 | "slab", 98 | "wasm-bindgen-futures", 99 | ] 100 | 101 | [[package]] 102 | name = "async-task" 103 | version = "4.3.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" 106 | 107 | [[package]] 108 | name = "atomic-waker" 109 | version = "1.0.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" 112 | 113 | [[package]] 114 | name = "autocfg" 115 | version = "1.1.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 118 | 119 | [[package]] 120 | name = "bacnet" 121 | version = "0.1.0" 122 | dependencies = [ 123 | "async-std", 124 | "byteorder", 125 | "bytes", 126 | "hex", 127 | "nom", 128 | "num-derive", 129 | "num-traits", 130 | "picky-asn1-der", 131 | "serde", 132 | "tracing", 133 | "tracing-subscriber", 134 | ] 135 | 136 | [[package]] 137 | name = "blocking" 138 | version = "1.3.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" 141 | dependencies = [ 142 | "async-channel", 143 | "async-lock", 144 | "async-task", 145 | "atomic-waker", 146 | "fastrand", 147 | "futures-lite", 148 | ] 149 | 150 | [[package]] 151 | name = "bumpalo" 152 | version = "3.11.1" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" 155 | 156 | [[package]] 157 | name = "byteorder" 158 | version = "1.4.3" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 161 | 162 | [[package]] 163 | name = "bytes" 164 | version = "1.3.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" 167 | 168 | [[package]] 169 | name = "cc" 170 | version = "1.0.77" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" 173 | 174 | [[package]] 175 | name = "cfg-if" 176 | version = "1.0.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 179 | 180 | [[package]] 181 | name = "concurrent-queue" 182 | version = "2.0.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" 185 | dependencies = [ 186 | "crossbeam-utils", 187 | ] 188 | 189 | [[package]] 190 | name = "crossbeam-utils" 191 | version = "0.8.14" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" 194 | dependencies = [ 195 | "cfg-if", 196 | ] 197 | 198 | [[package]] 199 | name = "ctor" 200 | version = "0.1.26" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" 203 | dependencies = [ 204 | "quote", 205 | "syn", 206 | ] 207 | 208 | [[package]] 209 | name = "event-listener" 210 | version = "2.5.3" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 213 | 214 | [[package]] 215 | name = "fastrand" 216 | version = "1.8.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 219 | dependencies = [ 220 | "instant", 221 | ] 222 | 223 | [[package]] 224 | name = "futures-channel" 225 | version = "0.3.25" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" 228 | dependencies = [ 229 | "futures-core", 230 | ] 231 | 232 | [[package]] 233 | name = "futures-core" 234 | version = "0.3.25" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" 237 | 238 | [[package]] 239 | name = "futures-io" 240 | version = "0.3.25" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" 243 | 244 | [[package]] 245 | name = "futures-lite" 246 | version = "1.12.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" 249 | dependencies = [ 250 | "fastrand", 251 | "futures-core", 252 | "futures-io", 253 | "memchr", 254 | "parking", 255 | "pin-project-lite", 256 | "waker-fn", 257 | ] 258 | 259 | [[package]] 260 | name = "gloo-timers" 261 | version = "0.2.5" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "98c4a8d6391675c6b2ee1a6c8d06e8e2d03605c44cec1270675985a4c2a5500b" 264 | dependencies = [ 265 | "futures-channel", 266 | "futures-core", 267 | "js-sys", 268 | "wasm-bindgen", 269 | ] 270 | 271 | [[package]] 272 | name = "hex" 273 | version = "0.4.3" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 276 | 277 | [[package]] 278 | name = "instant" 279 | version = "0.1.12" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 282 | dependencies = [ 283 | "cfg-if", 284 | ] 285 | 286 | [[package]] 287 | name = "js-sys" 288 | version = "0.3.60" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" 291 | dependencies = [ 292 | "wasm-bindgen", 293 | ] 294 | 295 | [[package]] 296 | name = "kv-log-macro" 297 | version = "1.0.7" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" 300 | dependencies = [ 301 | "log", 302 | ] 303 | 304 | [[package]] 305 | name = "lazy_static" 306 | version = "1.4.0" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 309 | 310 | [[package]] 311 | name = "libc" 312 | version = "0.2.138" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" 315 | 316 | [[package]] 317 | name = "log" 318 | version = "0.4.17" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 321 | dependencies = [ 322 | "cfg-if", 323 | "value-bag", 324 | ] 325 | 326 | [[package]] 327 | name = "memchr" 328 | version = "2.5.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 331 | 332 | [[package]] 333 | name = "minimal-lexical" 334 | version = "0.2.1" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 337 | 338 | [[package]] 339 | name = "nom" 340 | version = "7.1.1" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" 343 | dependencies = [ 344 | "memchr", 345 | "minimal-lexical", 346 | ] 347 | 348 | [[package]] 349 | name = "nu-ansi-term" 350 | version = "0.46.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 353 | dependencies = [ 354 | "overload", 355 | "winapi", 356 | ] 357 | 358 | [[package]] 359 | name = "num-derive" 360 | version = "0.3.3" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" 363 | dependencies = [ 364 | "proc-macro2", 365 | "quote", 366 | "syn", 367 | ] 368 | 369 | [[package]] 370 | name = "num-traits" 371 | version = "0.2.15" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 374 | dependencies = [ 375 | "autocfg", 376 | ] 377 | 378 | [[package]] 379 | name = "oid" 380 | version = "0.2.1" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2" 383 | dependencies = [ 384 | "serde", 385 | ] 386 | 387 | [[package]] 388 | name = "once_cell" 389 | version = "1.16.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 392 | 393 | [[package]] 394 | name = "overload" 395 | version = "0.1.1" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 398 | 399 | [[package]] 400 | name = "parking" 401 | version = "2.0.0" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" 404 | 405 | [[package]] 406 | name = "picky-asn1" 407 | version = "0.7.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "884997474f4d9e0f1a7079048e9f8a68d5a714612d9d7161836f3b5ca5cbae90" 410 | dependencies = [ 411 | "oid", 412 | "serde", 413 | "serde_bytes", 414 | ] 415 | 416 | [[package]] 417 | name = "picky-asn1-der" 418 | version = "0.4.0" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "e47267a46f4ea246b772381970b8ed3f15963dd3e15ffc2c3f4ac3bc2d77384b" 421 | dependencies = [ 422 | "picky-asn1", 423 | "serde", 424 | "serde_bytes", 425 | ] 426 | 427 | [[package]] 428 | name = "pin-project-lite" 429 | version = "0.2.9" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 432 | 433 | [[package]] 434 | name = "pin-utils" 435 | version = "0.1.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 438 | 439 | [[package]] 440 | name = "polling" 441 | version = "2.5.1" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" 444 | dependencies = [ 445 | "autocfg", 446 | "cfg-if", 447 | "libc", 448 | "log", 449 | "wepoll-ffi", 450 | "windows-sys", 451 | ] 452 | 453 | [[package]] 454 | name = "proc-macro2" 455 | version = "1.0.47" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 458 | dependencies = [ 459 | "unicode-ident", 460 | ] 461 | 462 | [[package]] 463 | name = "quote" 464 | version = "1.0.21" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 467 | dependencies = [ 468 | "proc-macro2", 469 | ] 470 | 471 | [[package]] 472 | name = "serde" 473 | version = "1.0.149" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" 476 | dependencies = [ 477 | "serde_derive", 478 | ] 479 | 480 | [[package]] 481 | name = "serde_bytes" 482 | version = "0.11.7" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" 485 | dependencies = [ 486 | "serde", 487 | ] 488 | 489 | [[package]] 490 | name = "serde_derive" 491 | version = "1.0.149" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" 494 | dependencies = [ 495 | "proc-macro2", 496 | "quote", 497 | "syn", 498 | ] 499 | 500 | [[package]] 501 | name = "sharded-slab" 502 | version = "0.1.4" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 505 | dependencies = [ 506 | "lazy_static", 507 | ] 508 | 509 | [[package]] 510 | name = "slab" 511 | version = "0.4.7" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 514 | dependencies = [ 515 | "autocfg", 516 | ] 517 | 518 | [[package]] 519 | name = "smallvec" 520 | version = "1.10.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 523 | 524 | [[package]] 525 | name = "socket2" 526 | version = "0.4.7" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 529 | dependencies = [ 530 | "libc", 531 | "winapi", 532 | ] 533 | 534 | [[package]] 535 | name = "syn" 536 | version = "1.0.105" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" 539 | dependencies = [ 540 | "proc-macro2", 541 | "quote", 542 | "unicode-ident", 543 | ] 544 | 545 | [[package]] 546 | name = "thread_local" 547 | version = "1.1.4" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 550 | dependencies = [ 551 | "once_cell", 552 | ] 553 | 554 | [[package]] 555 | name = "tracing" 556 | version = "0.1.37" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 559 | dependencies = [ 560 | "cfg-if", 561 | "pin-project-lite", 562 | "tracing-attributes", 563 | "tracing-core", 564 | ] 565 | 566 | [[package]] 567 | name = "tracing-attributes" 568 | version = "0.1.23" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" 571 | dependencies = [ 572 | "proc-macro2", 573 | "quote", 574 | "syn", 575 | ] 576 | 577 | [[package]] 578 | name = "tracing-core" 579 | version = "0.1.30" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 582 | dependencies = [ 583 | "once_cell", 584 | "valuable", 585 | ] 586 | 587 | [[package]] 588 | name = "tracing-log" 589 | version = "0.1.3" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 592 | dependencies = [ 593 | "lazy_static", 594 | "log", 595 | "tracing-core", 596 | ] 597 | 598 | [[package]] 599 | name = "tracing-subscriber" 600 | version = "0.3.16" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" 603 | dependencies = [ 604 | "nu-ansi-term", 605 | "sharded-slab", 606 | "smallvec", 607 | "thread_local", 608 | "tracing-core", 609 | "tracing-log", 610 | ] 611 | 612 | [[package]] 613 | name = "unicode-ident" 614 | version = "1.0.5" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 617 | 618 | [[package]] 619 | name = "valuable" 620 | version = "0.1.0" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 623 | 624 | [[package]] 625 | name = "value-bag" 626 | version = "1.0.0-alpha.9" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" 629 | dependencies = [ 630 | "ctor", 631 | "version_check", 632 | ] 633 | 634 | [[package]] 635 | name = "version_check" 636 | version = "0.9.4" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 639 | 640 | [[package]] 641 | name = "waker-fn" 642 | version = "1.1.0" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" 645 | 646 | [[package]] 647 | name = "wasm-bindgen" 648 | version = "0.2.83" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" 651 | dependencies = [ 652 | "cfg-if", 653 | "wasm-bindgen-macro", 654 | ] 655 | 656 | [[package]] 657 | name = "wasm-bindgen-backend" 658 | version = "0.2.83" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" 661 | dependencies = [ 662 | "bumpalo", 663 | "log", 664 | "once_cell", 665 | "proc-macro2", 666 | "quote", 667 | "syn", 668 | "wasm-bindgen-shared", 669 | ] 670 | 671 | [[package]] 672 | name = "wasm-bindgen-futures" 673 | version = "0.4.33" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" 676 | dependencies = [ 677 | "cfg-if", 678 | "js-sys", 679 | "wasm-bindgen", 680 | "web-sys", 681 | ] 682 | 683 | [[package]] 684 | name = "wasm-bindgen-macro" 685 | version = "0.2.83" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" 688 | dependencies = [ 689 | "quote", 690 | "wasm-bindgen-macro-support", 691 | ] 692 | 693 | [[package]] 694 | name = "wasm-bindgen-macro-support" 695 | version = "0.2.83" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" 698 | dependencies = [ 699 | "proc-macro2", 700 | "quote", 701 | "syn", 702 | "wasm-bindgen-backend", 703 | "wasm-bindgen-shared", 704 | ] 705 | 706 | [[package]] 707 | name = "wasm-bindgen-shared" 708 | version = "0.2.83" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 711 | 712 | [[package]] 713 | name = "web-sys" 714 | version = "0.3.60" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" 717 | dependencies = [ 718 | "js-sys", 719 | "wasm-bindgen", 720 | ] 721 | 722 | [[package]] 723 | name = "wepoll-ffi" 724 | version = "0.1.2" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" 727 | dependencies = [ 728 | "cc", 729 | ] 730 | 731 | [[package]] 732 | name = "winapi" 733 | version = "0.3.9" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 736 | dependencies = [ 737 | "winapi-i686-pc-windows-gnu", 738 | "winapi-x86_64-pc-windows-gnu", 739 | ] 740 | 741 | [[package]] 742 | name = "winapi-i686-pc-windows-gnu" 743 | version = "0.4.0" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 746 | 747 | [[package]] 748 | name = "winapi-x86_64-pc-windows-gnu" 749 | version = "0.4.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 752 | 753 | [[package]] 754 | name = "windows-sys" 755 | version = "0.42.0" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 758 | dependencies = [ 759 | "windows_aarch64_gnullvm", 760 | "windows_aarch64_msvc", 761 | "windows_i686_gnu", 762 | "windows_i686_msvc", 763 | "windows_x86_64_gnu", 764 | "windows_x86_64_gnullvm", 765 | "windows_x86_64_msvc", 766 | ] 767 | 768 | [[package]] 769 | name = "windows_aarch64_gnullvm" 770 | version = "0.42.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 773 | 774 | [[package]] 775 | name = "windows_aarch64_msvc" 776 | version = "0.42.0" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 779 | 780 | [[package]] 781 | name = "windows_i686_gnu" 782 | version = "0.42.0" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 785 | 786 | [[package]] 787 | name = "windows_i686_msvc" 788 | version = "0.42.0" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 791 | 792 | [[package]] 793 | name = "windows_x86_64_gnu" 794 | version = "0.42.0" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 797 | 798 | [[package]] 799 | name = "windows_x86_64_gnullvm" 800 | version = "0.42.0" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 803 | 804 | [[package]] 805 | name = "windows_x86_64_msvc" 806 | version = "0.42.0" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 809 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bacnet" 3 | version = "0.1.0" 4 | authors = ["Pascal Bach "] 5 | edition = "2018" 6 | license = "MIT/Apache-2.0" 7 | repository = "https://github.com/bachp/bacnet-rs" 8 | description = "A BACnet stack written in Rust." 9 | 10 | [dependencies] 11 | num-derive = "0.3" 12 | num-traits = "0.2" 13 | async-std = "1.8" 14 | tracing = "0.1" 15 | tracing-subscriber = "0.3" 16 | byteorder = "1.4" 17 | bytes = "1.0" 18 | picky-asn1-der = "0.4" 19 | serde = { version = "1.0", features = [ "derive" ] } 20 | nom = "7" 21 | hex ="0.4" 22 | 23 | [dev-dependencies] 24 | hex ="0.4" -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust BACnet stack 2 | 3 | > **This is currently in a very early development phase. It's intended to work as a walking skeleton and evolve in a complete BACnet implementation** 4 | 5 | Implementation of the BACnet (ASHRAE-135) standard in Rust. 6 | 7 | Some of the goals are: 8 | 9 | - Type safe BACnet telegram encoder and decoder 10 | - Work with an asynchronous network stacks 11 | - Work in a WebAssembly environment 12 | -------------------------------------------------------------------------------- /src/application.rs: -------------------------------------------------------------------------------- 1 | use crate::{Decode, Encode}; 2 | 3 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 4 | 5 | pub mod service; 6 | pub use service::*; 7 | 8 | use tracing::trace; 9 | 10 | /// BACnetPDU variants (Chapter 21) 11 | /// 12 | /// ```asn.1 13 | /// BACnetPDU ::= CHOICE { 14 | /// confirmed-request-pdu 15 | /// unconfirmed-request-pdu 16 | /// simple-ack-pdu 17 | /// complex-ack-pdu 18 | /// segment-ack-pdu 19 | /// error-pdu 20 | /// reject-pdu 21 | /// abort-pdu 22 | /// } 23 | /// ``` 24 | /// 25 | #[derive(Clone, Debug, Eq, PartialEq)] 26 | pub enum BACnetPDU { 27 | ConfirmedRequest, // = 0x00; 28 | UnconfirmedRequest, // = 0x01; 29 | SimpleACK, // = 0x02; 30 | ComplexACK, // = 0x03; 31 | SegmentACK, // = 0x04; 32 | Error, // = 0x05; 33 | Reject, // = 0x06; 34 | Abort, // = 0x07; 35 | } 36 | 37 | impl BACnetPDU { 38 | fn as_u8(&self) -> u8 { 39 | match self { 40 | Self::ConfirmedRequest => 0, 41 | Self::UnconfirmedRequest => 1, 42 | Self::SimpleACK => 2, 43 | Self::ComplexACK => 3, 44 | Self::SegmentACK => 4, 45 | Self::Error => 5, 46 | Self::Reject => 6, 47 | Self::Abort => 7, 48 | } 49 | } 50 | } 51 | 52 | /// BACnet-Unconfirmed-Request-PDU struct (Chapter 21) 53 | #[derive(Clone, Debug, Eq, PartialEq)] 54 | pub struct BACnetUnconfirmedRequestPDU {} 55 | 56 | #[derive(Clone, Debug, Eq, PartialEq)] 57 | pub struct APDU { 58 | apdu_type: u8, 59 | pub service_choice: u8, 60 | user_data: Vec, 61 | } 62 | 63 | impl APDU { 64 | pub fn new(apdu_type: u8, service_choice: u8, user_data: Vec) -> Self { 65 | Self { 66 | apdu_type, 67 | service_choice, 68 | user_data, 69 | } 70 | } 71 | } 72 | 73 | impl Encode for APDU { 74 | fn encode(&self, writer: &mut T) -> std::io::Result<()> { 75 | writer.write_u8(self.apdu_type << 4)?; 76 | writer.write_u8(self.service_choice)?; 77 | writer.write(&self.user_data)?; 78 | Ok(()) 79 | } 80 | 81 | fn len(&self) -> usize { 82 | let mut l = 0; 83 | l += 1; // Type 84 | l += 1; // Service Choice 85 | l += self.user_data.len(); // Content 86 | l 87 | } 88 | } 89 | 90 | impl Decode for APDU { 91 | fn decode(reader: &mut T) -> std::io::Result { 92 | let apdu_type = reader.read_u8()? >> 4; 93 | let service_choice = reader.read_u8()?; 94 | let mut content = Vec::new(); // TODO: What capacity? 95 | reader.read_to_end(&mut content)?; 96 | trace!("APDU Type: {}", apdu_type); 97 | Ok(APDU::new(apdu_type, service_choice, content)) 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | use super::*; 104 | use crate::{Decode, Encode}; 105 | use bytes::{BufMut, BytesMut}; 106 | use hex; 107 | 108 | use crate::tests::*; 109 | 110 | #[test] 111 | fn test_encode_apdu() { 112 | let content = vec![0, 0, 0]; 113 | let apdu = APDU::new(1, 8, content); 114 | 115 | let mut w = BytesMut::new().writer(); 116 | apdu.encode(&mut w).expect("Write APDU to buffer"); 117 | assert_eq!(w.into_inner().to_vec(), vec![16, 8, 0, 0, 0]); 118 | } 119 | 120 | #[test] 121 | fn test_who_is() { 122 | let mut data = hex::decode("1008").unwrap(); 123 | 124 | let apdu = APDU::decode(&mut std::io::Cursor::new(&mut data)).expect("Decode APDU"); 125 | 126 | assert_eq!(apdu.apdu_type, 0x01); 127 | assert_eq!(apdu.service_choice, 0x08); 128 | 129 | let mut w = BytesMut::new().writer(); 130 | apdu.encode(&mut w).expect("Write APDU to buffer"); 131 | assert_eq!(w.into_inner().to_vec(), data); 132 | } 133 | 134 | #[test] 135 | fn test_i_am() { 136 | let mut data = hex::decode("1000c4020002572204009100210f").unwrap(); 137 | 138 | let apdu = APDU::decode(&mut std::io::Cursor::new(&mut data)).expect("Decode APDU"); 139 | 140 | assert_eq!(apdu.apdu_type, 0x01); 141 | assert_eq!(apdu.service_choice, 0x00); 142 | assert_eq!( 143 | apdu.user_data, 144 | vec![196, 2, 0, 2, 87, 34, 4, 0, 145, 0, 33, 15] 145 | ); 146 | 147 | let mut w = BytesMut::new().writer(); 148 | apdu.encode(&mut w).expect("Write APDU to buffer"); 149 | assert_eq!(w.into_inner().to_vec(), data); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/application/service.rs: -------------------------------------------------------------------------------- 1 | use crate::{Decode, Encode}; 2 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 3 | 4 | #[derive(Clone, Debug, Eq, PartialEq)] 5 | pub enum Service {} 6 | 7 | #[derive(Clone, Debug, Eq, PartialEq)] 8 | pub enum UnconfirmedService { 9 | IAm(IAm), // = 0; 10 | IHave, // = 1; 11 | UnconfirmedCovNotification, // = 2; 12 | UnconfirmedEventNotification, // = 3; 13 | UnconfirmedPrivateTransfer, // = 4; 14 | UnconfirmedTextMessage, // = 5; 15 | TimeSynchronization, // = 6; 16 | WhoHas, // = 7; 17 | WhoIs(), // = 8; 18 | UtcTimeSynchronization, // = 9; 19 | WriteGroup, // = 10; 20 | UnconfirmedCovNotificationMultiple, // = 11; 21 | } 22 | 23 | impl Decode for UnconfirmedService { 24 | fn decode(reader: &mut T) -> std::io::Result { 25 | // TODO: Add checks 26 | let type_ = reader.read_u8()?; 27 | 28 | match type_ { 29 | 0x00 => Ok(Self::IAm(IAm::decode(reader)?)), 30 | 0x08 => Ok(Self::WhoIs()), 31 | t => unimplemented!(), 32 | } 33 | } 34 | } 35 | 36 | impl Encode for UnconfirmedService { 37 | fn encode(&self, writer: &mut T) -> std::io::Result<()> { 38 | match self { 39 | Self::IAm(a) => a.encode(writer), 40 | Self::WhoIs() => Ok(()), 41 | _ => unimplemented!(), 42 | } 43 | } 44 | 45 | fn len(&self) -> usize { 46 | match self { 47 | Self::IAm(a) => a.len(), 48 | Self::WhoIs() => 0, 49 | _ => unimplemented!(), 50 | } 51 | } 52 | } 53 | 54 | #[derive(Clone, Debug, Eq, PartialEq)] 55 | pub struct IAm {} 56 | 57 | impl Decode for IAm { 58 | fn decode(_reader: &mut T) -> std::io::Result { 59 | Ok(Self {}) 60 | } 61 | } 62 | 63 | impl Encode for IAm { 64 | fn encode(&self, writer: &mut T) -> std::io::Result<()> { 65 | let data = vec![196, 2, 0, 2, 87, 34, 4, 0, 145, 0, 33, 15]; 66 | writer.write(&data)?; 67 | Ok(()) 68 | } 69 | 70 | fn len(&self) -> usize { 71 | 12 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/encoding.rs: -------------------------------------------------------------------------------- 1 | mod parse; 2 | 3 | use nom::IResult; 4 | 5 | pub struct Tag<'a> { 6 | tag_number: TagNumber, 7 | lvt: LengthValueType, 8 | data: &'a [u8], 9 | } 10 | pub enum TagNumber { 11 | Application(ApplicationTag), 12 | Context(ContextTag), 13 | } 14 | pub enum LengthValueType { 15 | Length(u32), 16 | Value(u8), 17 | Opening, 18 | Closing, 19 | } 20 | 21 | pub enum ApplicationTag { 22 | Null, //= 0, 23 | Boolean, //= 1, 24 | UnsignedInteger, //= 2, 25 | SignedInteger, //= 3, // (2's complement notation) 26 | Real, //= 4, // (ANSI/IEEE-754 floating point) 27 | Double, //= 5, // (ANSI/IEEE-754 double precision floating point) 28 | OctetString, //= 6, 29 | CharacterString, //= 7, 30 | BitString, //= 8, 31 | Enumerated, //= 9, 32 | Date, //= 10, 33 | Time, //= 11, 34 | BACnetObjectIdentifier, //= 12, 35 | Reserved(u8), //= 13, 14, 15 // Reserved for ASHRAE 36 | Other(u8), 37 | } 38 | 39 | impl From for ApplicationTag { 40 | fn from(tag_number: u8) -> Self { 41 | match tag_number { 42 | 0 => ApplicationTag::Null, 43 | 1 => ApplicationTag::Boolean, 44 | 2 => ApplicationTag::UnsignedInteger, 45 | 3 => ApplicationTag::SignedInteger, 46 | 4 => ApplicationTag::Real, 47 | 5 => ApplicationTag::Double, 48 | 6 => ApplicationTag::OctetString, 49 | 7 => ApplicationTag::CharacterString, 50 | 8 => ApplicationTag::BitString, 51 | 9 => ApplicationTag::Enumerated, 52 | 10 => ApplicationTag::Date, 53 | 11 => ApplicationTag::Time, 54 | 12 => ApplicationTag::BACnetObjectIdentifier, 55 | t @ 13..=15 => ApplicationTag::Reserved(t), 56 | t => ApplicationTag::Other(t), 57 | } 58 | } 59 | } 60 | 61 | impl Into for ApplicationTag { 62 | fn into(self) -> u8 { 63 | match self { 64 | ApplicationTag::Null => 0, 65 | ApplicationTag::Boolean => 1, 66 | ApplicationTag::UnsignedInteger => 2, 67 | ApplicationTag::SignedInteger => 3, 68 | ApplicationTag::Real => 4, 69 | ApplicationTag::Double => 5, 70 | ApplicationTag::OctetString => 6, 71 | ApplicationTag::CharacterString => 7, 72 | ApplicationTag::BitString => 8, 73 | ApplicationTag::Enumerated => 9, 74 | ApplicationTag::Date => 10, 75 | ApplicationTag::Time => 11, 76 | ApplicationTag::BACnetObjectIdentifier => 12, 77 | ApplicationTag::Reserved(t) => t, 78 | ApplicationTag::Other(t) => t, 79 | } 80 | } 81 | } 82 | 83 | pub enum ContextTag { 84 | Other(u8), 85 | } 86 | 87 | impl From for ContextTag { 88 | fn from(tag_number: u8) -> Self { 89 | match tag_number { 90 | t => ContextTag::Other(t), 91 | } 92 | } 93 | } 94 | 95 | impl Into for ContextTag { 96 | fn into(self) -> u8 { 97 | match self { 98 | ContextTag::Other(t) => t, 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/encoding/parse.rs: -------------------------------------------------------------------------------- 1 | use nom::number::streaming::{be_f64, be_i16, be_i24, be_u16, be_u24, be_u32, be_u8}; 2 | use nom::{Err, IResult, Needed}; 3 | use std::io::Cursor; 4 | 5 | use crate::encoding::{ApplicationTag, ContextTag, LengthValueType, Tag, TagNumber}; 6 | 7 | pub fn parse_bacnet_tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Tag> { 8 | let mut cur = Cursor::new(input); 9 | let first_byte = cur.get_u8(); 10 | let tag_number = (first_byte & 0b1111_0_000) >> 4; 11 | 12 | // 20.2.1.2 Tag Number 13 | let tag_number = match tag_number { 14 | t @ 0..=14 => t, 15 | 15..=255 => cur.get_u8(), 16 | }; 17 | 18 | // 20.2.1.1 Class 19 | let class = (first_byte & 0b0000_1_000) != 0; 20 | let tag_number = match class { 21 | false => TagNumber::Application(ApplicationTag::from(tag_number)), 22 | true => TagNumber::Context(ContextTag::from(tag_number)), 23 | }; 24 | 25 | // 20.2.1.3 Length/Value/Type 26 | let lvt = first_byte & 0b0000_0_111; 27 | let lvt = match lvt { 28 | l if std::matches!(tag_number, TagNumber::Application(ApplicationTag::Boolean)) => { 29 | LengthValueType::Value(l) 30 | } 31 | l if l < 0b101 => LengthValueType::Length(l as u32), 32 | 0b101 => { 33 | let extended = cur.get_u8(); 34 | let length = match extended { 35 | l @ 0..=253 => l as u32, 36 | 254 => cur.get_u16() as u32, 37 | 255 => cur.get_u32(), 38 | }; 39 | LengthValueType::Length(length) 40 | } 41 | 0b110 => LengthValueType::Opening, 42 | 0b111 => LengthValueType::Closing, 43 | _ => unreachable!("Length is only 3 bits"), 44 | }; 45 | 46 | let data_start = cur.position() as usize; 47 | let mut data_end = data_start; 48 | 49 | if let LengthValueType::Length(l) = lvt { 50 | data_end += l as usize; 51 | } 52 | 53 | // TODO: Throw a proper error if slice is not long enough, currently it just panicks which is still safe 54 | 55 | let data = &input[data_start..data_end]; 56 | let output = &input[data_end..]; 57 | 58 | let tag = Tag { 59 | tag_number, 60 | lvt, 61 | data, 62 | }; 63 | 64 | Ok((output, tag)) 65 | } 66 | 67 | use bytes::{Buf, BufMut}; 68 | 69 | pub fn decode_buf<'a>(buf: &'a [u8]) -> Result<(u8, bool, u32, &'a [u8]), String> { 70 | let mut cur = Cursor::new(buf); 71 | 72 | let first_byte = cur.get_u8(); 73 | let tag_number = (first_byte & 0b1111_0_000) >> 4; 74 | 75 | // 20.2.1.2 Tag Number 76 | let tag_number = match tag_number { 77 | t @ 0..=14 => t, 78 | 15..=255 => cur.get_u8(), 79 | }; 80 | 81 | // 20.2.1.1 Class 82 | let class = (first_byte & 0b0000_1_000) != 0; 83 | 84 | // 20.2.1.3 Length/Value/Type 85 | let length = first_byte & 0b0000_0_111; 86 | let length: u32 = if length < 0b101 { 87 | length as u32 88 | } else { 89 | let extended = cur.get_u8(); 90 | match extended { 91 | l @ 0..=253 => l as u32, 92 | 254 => cur.get_u16() as u32, 93 | 255 => cur.get_u32(), 94 | } 95 | }; 96 | 97 | // Offset where the data starts, 98 | // depends on how length is encoded 99 | let offset = cur.position() as usize; 100 | 101 | // TODO: Throw a proper error if slice is not long enough, currently it just panicks which is still safe 102 | let data = &buf[offset..offset + (length as usize)]; 103 | 104 | Ok((tag_number, class, length, data)) 105 | } 106 | 107 | pub fn encode_buf(tag_number: u8, class: bool, length: u32) -> Result, String> { 108 | let mut buf: Vec = vec![0x00]; // Initial tag set to zero so we can do bitwise or 109 | 110 | match tag_number { 111 | t @ 0..=14 => { 112 | buf[0] |= t << 4; 113 | } 114 | t @ 15..=255 => { 115 | buf[0] |= 0b1111 << 4; 116 | buf.put_u8(t); 117 | } 118 | }; 119 | 120 | if class { 121 | buf[0] |= 0b0000_1_000; 122 | } 123 | 124 | match length { 125 | l @ 0..=4 => { 126 | buf[0] |= l as u8; 127 | } 128 | l @ 5..=253 => { 129 | buf[0] |= 0b101; 130 | buf.put_u8(l as u8); 131 | } 132 | l @ 254..=65535 => { 133 | buf[0] |= 0b101; 134 | buf.put_u8(254); 135 | buf.put_u16(l as u16); 136 | } 137 | l @ 65536..=core::u32::MAX => { 138 | buf[0] |= 0b101; 139 | buf.put_u8(255); 140 | buf.put_u32(l as u32); 141 | } 142 | } 143 | Ok(buf) 144 | } 145 | 146 | #[cfg(test)] 147 | mod tests { 148 | use super::*; 149 | use bytes::BufMut; 150 | use bytes::{Bytes, BytesMut}; 151 | use hex; 152 | use std::matches; 153 | 154 | #[test] 155 | /// ASN.1 = NULL 156 | fn test_parse_application_tag_null() { 157 | let input: &[u8] = &[0b0000_0_000]; 158 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 159 | assert!(matches!( 160 | tag.tag_number, 161 | TagNumber::Application(ApplicationTag::Null) 162 | )); 163 | assert!(matches!(tag.lvt, LengthValueType::Length(0))); 164 | assert!(tag.data.is_empty()); 165 | } 166 | 167 | #[test] 168 | /// ASN.1 = [3] NULL 169 | fn test_parse_context_tag_3_null() { 170 | let input: &[u8] = &[0x38]; 171 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 172 | assert!(matches!( 173 | tag.tag_number, 174 | TagNumber::Context(ContextTag::Other(3)) 175 | )); 176 | assert!(matches!(tag.lvt, LengthValueType::Length(0))); 177 | assert!(tag.data.is_empty()); 178 | } 179 | 180 | #[test] 181 | /// ASN.1 = BOOLEAN 182 | /// Value = FALSE 183 | fn test_parse_application_tag_boolean_false() { 184 | let input: &[u8] = &[0b0001_0_000]; 185 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 186 | assert!(matches!( 187 | tag.tag_number, 188 | TagNumber::Application(ApplicationTag::Boolean) 189 | )); 190 | assert!(matches!(tag.lvt, LengthValueType::Value(0))); 191 | assert!(tag.data.is_empty()); 192 | } 193 | 194 | #[test] 195 | /// ASN.1 = BOOLEAN 196 | /// Value = TRUE 197 | fn test_parse_application_tag_boolean_true() { 198 | let input: &[u8] = &[0b0001_0_001]; 199 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 200 | assert!(matches!( 201 | tag.tag_number, 202 | TagNumber::Application(ApplicationTag::Boolean) 203 | )); 204 | assert!(matches!(tag.lvt, LengthValueType::Value(1))); 205 | assert!(tag.data.is_empty()); 206 | } 207 | 208 | #[test] 209 | /// ASN.1 = [2] BOOLEAN 210 | /// Value = FALSE 211 | fn test_parse_context_tag_2_boolean_false() { 212 | let input: &[u8] = &[0x29, 0x00]; 213 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 214 | assert!(matches!( 215 | tag.tag_number, 216 | TagNumber::Context(ContextTag::Other(2)) 217 | )); 218 | assert!(matches!(tag.lvt, LengthValueType::Length(1))); 219 | assert_eq!(tag.data, &[0b0000_0000]); 220 | } 221 | 222 | #[test] 223 | /// ASN.1 = [2] BOOLEAN 224 | /// Value = TRUE 225 | fn test_parse_context_tag_2_boolean_true() { 226 | let input: &[u8] = &[0x29, 0x01]; 227 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 228 | assert!(matches!( 229 | tag.tag_number, 230 | TagNumber::Context(ContextTag::Other(2)) 231 | )); 232 | assert!(matches!(tag.lvt, LengthValueType::Length(1))); 233 | assert_eq!(tag.data, &[0b0000_0001]); 234 | } 235 | 236 | #[test] 237 | /// ASN.1 = [6] BOOLEAN 238 | /// Value = FALSE 239 | fn test_parse_context_tag_6_boolean_false() { 240 | let input: &[u8] = &[0x69, 0x00]; 241 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 242 | assert!(matches!( 243 | tag.tag_number, 244 | TagNumber::Context(ContextTag::Other(6)) 245 | )); 246 | assert!(matches!(tag.lvt, LengthValueType::Length(1))); 247 | assert_eq!(tag.data, &[0b0000_0000]); 248 | } 249 | 250 | #[test] 251 | /// ASN.1 = [27] BOOLEAN 252 | /// Value = FALSE 253 | fn test_parse_context_tag_27_boolean_false() { 254 | let input: &[u8] = &[0xF9, 0x1b, 0x00]; 255 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 256 | assert!(matches!( 257 | tag.tag_number, 258 | TagNumber::Context(ContextTag::Other(27)) 259 | )); 260 | assert!(matches!(tag.lvt, LengthValueType::Length(1))); 261 | assert_eq!(tag.data, &[0b0000_0000]); 262 | } 263 | 264 | #[test] 265 | /// ASN.1 = Unsigned 266 | /// Value = 72 267 | fn test_parse_application_tag_unsigned_integer_72() { 268 | let input: &[u8] = &[0x21, 0x48]; 269 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 270 | assert!(matches!( 271 | tag.tag_number, 272 | TagNumber::Application(ApplicationTag::UnsignedInteger) 273 | )); 274 | assert!(matches!(tag.lvt, LengthValueType::Length(1))); 275 | assert_eq!(tag.data, &[72]); 276 | } 277 | 278 | #[test] 279 | /// ASN.1 = [0] Unsigned 280 | /// Value = 256 281 | fn test_parse_context_tag_0_unsigned_integer_256() { 282 | let input: &[u8] = &[0x0A, 0x01, 0x00]; 283 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 284 | assert!(matches!( 285 | tag.tag_number, 286 | TagNumber::Context(ContextTag::Other(0)) 287 | )); 288 | assert!(matches!(tag.lvt, LengthValueType::Length(2))); 289 | assert_eq!(tag.data, &[0x01, 0x00]); 290 | } 291 | 292 | #[test] 293 | /// ASN.1 = INTEGER 294 | /// Value = 72 295 | fn test_parse_application_tag_signed_integer_72() { 296 | let input: &[u8] = &[0x31, 0x48]; 297 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 298 | assert!(matches!( 299 | tag.tag_number, 300 | TagNumber::Application(ApplicationTag::SignedInteger) 301 | )); 302 | assert!(matches!(tag.lvt, LengthValueType::Length(1))); 303 | assert_eq!(tag.data, &[72]); 304 | } 305 | 306 | 307 | #[test] 308 | /// ASN.1 = [5] INTEGER 309 | /// Value = -72 310 | fn test_parse_context_tag_5_signed_integer_72() { 311 | let input: &[u8] = &[0x59, 0xB8]; 312 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 313 | assert!(matches!( 314 | tag.tag_number, 315 | TagNumber::Context(ContextTag::Other(5)) 316 | )); 317 | assert!(matches!(tag.lvt, LengthValueType::Length(1))); 318 | assert_eq!(tag.data, &[0xB8]); 319 | } 320 | 321 | #[test] 322 | /// ASN.1 = [33] INTEGER 323 | /// Value = -72 324 | fn test_parse_context_tag_33_signed_integer_72() { 325 | let input: &[u8] = &[0xF9, 0x21, 0xB8]; 326 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 327 | assert!(matches!( 328 | tag.tag_number, 329 | TagNumber::Context(ContextTag::Other(33)) 330 | )); 331 | assert!(matches!(tag.lvt, LengthValueType::Length(1))); 332 | assert_eq!(tag.data, &[0xB8]); 333 | } 334 | 335 | #[test] 336 | /// ASN.1 = REAL 337 | /// Value = 72.0 338 | fn test_parse_application_tag_real_72_0() { 339 | let input: &[u8] = &[0x44, 0x42, 0x90, 0x00, 0x00]; 340 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 341 | assert!(matches!( 342 | tag.tag_number, 343 | TagNumber::Application(ApplicationTag::Real) 344 | )); 345 | assert!(matches!(tag.lvt, LengthValueType::Length(4))); 346 | assert_eq!(tag.data, &[0x42, 0x90, 0x00, 0x00]); 347 | } 348 | 349 | #[test] 350 | /// ASN.1 = [0] REAL 351 | /// Value = -33.3 352 | fn test_parse_context_tag_0_real_33_3() { 353 | let input: &[u8] = &[0x0C, 0xC2, 0x05, 0x33, 0x33]; 354 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 355 | assert!(matches!( 356 | tag.tag_number, 357 | TagNumber::Context(ContextTag::Other(0)) 358 | )); 359 | assert!(matches!(tag.lvt, LengthValueType::Length(4))); 360 | assert_eq!(tag.data, &[0xC2, 0x05, 0x33, 0x33]); 361 | } 362 | 363 | #[test] 364 | /// ASN.1 = Double 365 | /// Value = 72.0 366 | fn test_parse_application_tag_double_72_0() { 367 | let input: &[u8] = &[0x55, 0x08, 0x40, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; 368 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 369 | assert!(matches!( 370 | tag.tag_number, 371 | TagNumber::Application(ApplicationTag::Double) 372 | )); 373 | assert!(matches!(tag.lvt, LengthValueType::Length(8))); 374 | assert_eq!(tag.data, &[0x40, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); 375 | } 376 | 377 | #[test] 378 | /// ASN.1 = [1] Double 379 | /// Value = -33.3 380 | fn test_parse_context_tag_1_double_33_3() { 381 | let input: &[u8] = &[0x1D, 0x08, 0xC0, 0x40, 0xA6, 0x66, 0x66, 0x66, 0x66, 0x66]; 382 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 383 | assert!(matches!( 384 | tag.tag_number, 385 | TagNumber::Context(ContextTag::Other(1)) 386 | )); 387 | assert!(matches!(tag.lvt, LengthValueType::Length(8))); 388 | assert_eq!(tag.data, &[0xC0, 0x40, 0xA6, 0x66, 0x66, 0x66, 0x66, 0x66]); 389 | } 390 | 391 | #[test] 392 | /// ASN.1 = [85] Double 393 | /// Value = -33.3 394 | fn test_parse_context_tag_85_double_33_3() { 395 | let input: &[u8] = &[0xFD, 0x55, 0x08, 0xC0, 0x40, 0xA6, 0x66, 0x66, 0x66, 0x66, 0x66]; 396 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 397 | assert!(matches!( 398 | tag.tag_number, 399 | TagNumber::Context(ContextTag::Other(85)) 400 | )); 401 | assert!(matches!(tag.lvt, LengthValueType::Length(8))); 402 | assert_eq!(tag.data, &[0xC0, 0x40, 0xA6, 0x66, 0x66, 0x66, 0x66, 0x66]); 403 | } 404 | 405 | #[test] 406 | /// ASN.1 = OCTET STRING 407 | /// Value = X'1234FF' 408 | fn test_parse_application_tag_octet_string_example() { 409 | let input: &[u8] = &[0x63, 0x12, 0x34, 0xFF]; 410 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 411 | assert!(matches!( 412 | tag.tag_number, 413 | TagNumber::Application(ApplicationTag::OctetString) 414 | )); 415 | assert!(matches!(tag.lvt, LengthValueType::Length(3))); 416 | assert_eq!(tag.data, &[0x12, 0x34, 0xFF]); 417 | } 418 | 419 | #[test] 420 | /// ASN.1 = [1] OCTET STRING 421 | /// Value = X'4321' 422 | fn test_parse_context_tag_1_octet_string_example() { 423 | let input: &[u8] = &[0x1A, 0x43, 0x21]; 424 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 425 | assert!(matches!( 426 | tag.tag_number, 427 | TagNumber::Context(ContextTag::Other(1)) 428 | )); 429 | assert!(matches!(tag.lvt, LengthValueType::Length(2))); 430 | assert_eq!(tag.data, &[0x43, 0x21]); 431 | } 432 | 433 | #[test] 434 | fn test_parse_application_tag_character_string_utf8() { 435 | let mut input = BytesMut::from(&[0x75, 0x19, 0x00][..]); 436 | input.extend_from_slice( 437 | &hex::decode("546869732069732061204241436E657420737472696E6721").unwrap(), 438 | ); 439 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 440 | assert!(matches!( 441 | tag.tag_number, 442 | TagNumber::Application(ApplicationTag::CharacterString) 443 | )); 444 | assert!(matches!(tag.lvt, LengthValueType::Length(25))); 445 | let mut ref_data = BytesMut::from(&[0x00][..]); 446 | ref_data.extend_from_slice("This is a BACnet string!".as_bytes()); 447 | assert_eq!(tag.data, ref_data); 448 | } 449 | 450 | #[test] 451 | fn test_parse_application_tag_character_string_utf8_non_ascii() { 452 | let mut input = BytesMut::from(&[0x75, 0x0A, 0x00][..]); 453 | input.extend_from_slice(&hex::decode("4672616EC3A7616973").unwrap()); 454 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 455 | assert!(matches!( 456 | tag.tag_number, 457 | TagNumber::Application(ApplicationTag::CharacterString) 458 | )); 459 | assert!(matches!(tag.lvt, LengthValueType::Length(10))); 460 | let mut ref_data = BytesMut::from(&[0x00][..]); 461 | ref_data.extend_from_slice("Français".as_bytes()); 462 | assert_eq!(tag.data, ref_data); 463 | } 464 | 465 | #[test] 466 | fn test_parse_application_tag_character_string_dbcs() { 467 | let mut input = BytesMut::from(&[0x75, 0x1B][..]); 468 | input.extend_from_slice( 469 | &hex::decode("010352546869732069732061204241436E657420737472696E6721").unwrap(), 470 | ); 471 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 472 | assert!(matches!( 473 | tag.tag_number, 474 | TagNumber::Application(ApplicationTag::CharacterString) 475 | )); 476 | assert!(matches!(tag.lvt, LengthValueType::Length(27))); 477 | let ref_data = 478 | &hex::decode("010352546869732069732061204241436E657420737472696E6721").unwrap(); 479 | assert_eq!(tag.data, ref_data); 480 | } 481 | 482 | #[test] 483 | fn test_parse_application_tag_character_string_ucs2() { 484 | let mut input = BytesMut::from(&[0x75, 0x31][..]); 485 | input.extend_from_slice(&hex::decode("040054006800690073002000690073002000610020004200410043006E0065007400200073007400720069006E00670021").unwrap()); 486 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 487 | assert!(matches!( 488 | tag.tag_number, 489 | TagNumber::Application(ApplicationTag::CharacterString) 490 | )); 491 | assert!(matches!(tag.lvt, LengthValueType::Length(49))); 492 | let ref_data = &hex::decode("040054006800690073002000690073002000610020004200410043006E0065007400200073007400720069006E00670021").unwrap(); 493 | assert_eq!(tag.data, ref_data); 494 | } 495 | 496 | #[test] 497 | fn test_parse_application_tag_bit_string() { 498 | let input: &[u8] = &[0x82, 0x03, 0xA8]; 499 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 500 | assert!(matches!( 501 | tag.tag_number, 502 | TagNumber::Application(ApplicationTag::BitString) 503 | )); 504 | assert!(matches!(tag.lvt, LengthValueType::Length(2))); 505 | assert_eq!(tag.data, &[0x03, 0xA8]); 506 | } 507 | 508 | #[test] 509 | fn test_parse_application_tag_enumerated_analog_input() { 510 | let input: &[u8] = &[0x91, 0x00]; 511 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 512 | assert!(matches!( 513 | tag.tag_number, 514 | TagNumber::Application(ApplicationTag::Enumerated) 515 | )); 516 | assert!(matches!(tag.lvt, LengthValueType::Length(1))); 517 | assert_eq!(tag.data, &[0x00]); 518 | } 519 | 520 | #[test] 521 | fn test_parse_application_tag_date_specific_value() { 522 | let input: &[u8] = &[0xA4, 0x5B, 0x01, 0x18, 0x04]; 523 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 524 | assert!(matches!( 525 | tag.tag_number, 526 | TagNumber::Application(ApplicationTag::Date) 527 | )); 528 | assert!(matches!(tag.lvt, LengthValueType::Length(4))); 529 | assert_eq!(tag.data, &[0x5B, 0x01, 0x18, 0x04]); 530 | } 531 | 532 | #[test] 533 | fn test_parse_application_tag_date_pattern() { 534 | let input: &[u8] = &[0xA4, 0x5B, 0xFF, 0x18, 0xFF]; 535 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 536 | assert!(matches!( 537 | tag.tag_number, 538 | TagNumber::Application(ApplicationTag::Date) 539 | )); 540 | assert!(matches!(tag.lvt, LengthValueType::Length(4))); 541 | assert_eq!(tag.data, &[0x5B, 0xFF, 0x18, 0xFF]); 542 | } 543 | 544 | #[test] 545 | fn test_parse_application_tag_time_specific_value() { 546 | let input: &[u8] = &[0xB4, 0x11, 0x23, 0x2D, 0x11]; 547 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 548 | assert!(matches!( 549 | tag.tag_number, 550 | TagNumber::Application(ApplicationTag::Time) 551 | )); 552 | assert!(matches!(tag.lvt, LengthValueType::Length(4))); 553 | assert_eq!(tag.data, &[0x11, 0x23, 0x2D, 0x11]); 554 | } 555 | 556 | #[test] 557 | fn test_parse_application_bacnet_object_identifier() { 558 | let input: &[u8] = &[0xC4, 0x00, 0xC0, 0x00, 0x0F]; 559 | let (_, tag) = parse_bacnet_tag(input).unwrap(); 560 | assert!(matches!( 561 | tag.tag_number, 562 | TagNumber::Application(ApplicationTag::BACnetObjectIdentifier) 563 | )); 564 | assert!(matches!(tag.lvt, LengthValueType::Length(4))); 565 | assert_eq!(tag.data, &[0x00, 0xC0, 0x00, 0x0F]); 566 | } 567 | 568 | #[test] 569 | fn test_decode_application_tag_number_0() { 570 | let (_, tag) = parse_bacnet_tag(&[0b0000_0_000]).unwrap(); 571 | assert!(matches!( 572 | tag.tag_number, 573 | TagNumber::Application(ApplicationTag::Null) 574 | )); 575 | } 576 | 577 | #[test] 578 | fn test_decode_context_tag_number_0() { 579 | let (_, tag) = parse_bacnet_tag(&[0b0000_1_000]).unwrap(); 580 | assert!(matches!( 581 | tag.tag_number, 582 | TagNumber::Context(ContextTag::Other(0)) 583 | )); 584 | } 585 | 586 | #[test] 587 | fn test_decode_application_tag_number_14() { 588 | let (_, tag) = parse_bacnet_tag(&[0b1110_0_000]).unwrap(); 589 | assert!(matches!( 590 | tag.tag_number, 591 | TagNumber::Application(ApplicationTag::Reserved(14)) 592 | )); 593 | } 594 | 595 | #[test] 596 | fn test_decode_application_tag_number_15() { 597 | let (_, tag) = parse_bacnet_tag(&[0b1111_0_000, 15]).unwrap(); 598 | assert!(matches!( 599 | tag.tag_number, 600 | TagNumber::Application(ApplicationTag::Reserved(15)) 601 | )); 602 | } 603 | 604 | #[test] 605 | fn test_decode_application_tag_number_254() { 606 | let (_, tag) = parse_bacnet_tag(&[0b1111_0_000, 254]).unwrap(); 607 | assert!(matches!( 608 | tag.tag_number, 609 | TagNumber::Application(ApplicationTag::Other(254)) 610 | )); 611 | } 612 | 613 | #[test] 614 | fn test_decode_application_reserved_tag_number_255() { 615 | let (_, tag) = parse_bacnet_tag(&[0b1111_0_000, 255]).unwrap(); 616 | assert!(matches!( 617 | tag.tag_number, 618 | TagNumber::Application(ApplicationTag::Other(255)) 619 | )); 620 | } 621 | 622 | #[test] 623 | fn test_decode_length_0() { 624 | let (_, tag) = parse_bacnet_tag(&[0b0000_0_000]).unwrap(); 625 | assert!(matches!(tag.lvt, LengthValueType::Length(0))); 626 | } 627 | 628 | #[test] 629 | fn test_decode_length_4() { 630 | let (_, tag) = parse_bacnet_tag(&[0b0000_0_100, 0, 0, 0, 0]).unwrap(); 631 | assert!(matches!(tag.lvt, LengthValueType::Length(4))); 632 | } 633 | 634 | #[test] 635 | fn test_decode_length_5() { 636 | let mut input = BytesMut::from(&[0b0000_0_101, 5][..]); 637 | input.extend_from_slice(&[0u8; 5][..]); 638 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 639 | assert!(matches!(tag.lvt, LengthValueType::Length(5))); 640 | } 641 | 642 | #[test] 643 | fn test_decode_length_253() { 644 | let mut input = BytesMut::from(&[0b0000_0_101, 253][..]); 645 | input.extend_from_slice(&[0u8; 253][..]); 646 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 647 | assert!(matches!(tag.lvt, LengthValueType::Length(253))); 648 | } 649 | 650 | #[test] 651 | fn test_decode_length_254() { 652 | let mut input = BytesMut::from(&[0b0000_0_101, 254, 0, 254][..]); 653 | input.extend_from_slice(&[0u8; 254][..]); 654 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 655 | assert!(matches!(tag.lvt, LengthValueType::Length(254))); 656 | } 657 | 658 | #[test] 659 | fn test_decode_length_65535() { 660 | let mut input = BytesMut::from(&[0b0000_0_101, 254, 255, 255][..]); 661 | input.extend_from_slice(&[0u8; 65535][..]); 662 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 663 | assert!(matches!(tag.lvt, LengthValueType::Length(65535))); 664 | } 665 | 666 | #[test] 667 | fn test_length_65536() { 668 | let mut input = BytesMut::from(&[0b0000_0_101, 255, 0, 1, 0, 0][..]); 669 | input.extend_from_slice(&[0u8; 65536][..]); 670 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 671 | assert!(matches!(tag.lvt, LengthValueType::Length(65536))); 672 | } 673 | 674 | /* TODO: These tests require to much memory! Find a better way to test them 675 | #[test] 676 | fn test_decode_length_u32max_minus_1() { 677 | let mut input = BytesMut::from(&[0b0000_0_101, 255, 255, 255, 255, 254][..]); 678 | input.extend_from_slice(&[0u8; (std::u32::MAX - 1) as usize][..]); 679 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 680 | if let LengthValueType::Length(l) = tag.lvt { 681 | assert_eq!(l, std::u32::MAX - 1); 682 | } else { 683 | panic!("Not a LengthValueType::Length"); 684 | }; 685 | } 686 | 687 | #[test] 688 | fn test_reserved_length_u32max() { 689 | let mut input = BytesMut::from(&[0b0000_0_101, 255, 255, 255, 255, 255][..]); 690 | input.extend_from_slice(&[0u8; std::u32::MAX as usize][..]); 691 | let (_, tag) = parse_bacnet_tag(&input).unwrap(); 692 | assert!(matches!(tag.lvt, LengthValueType::Length(std::u32::MAX))); 693 | }*/ 694 | } 695 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod application; 2 | pub mod encoding; 3 | pub mod network; 4 | pub mod transport; 5 | 6 | pub trait Decode { 7 | fn decode(reader: &mut T) -> std::io::Result; 8 | 9 | fn decode_slice(slice: &[u8]) -> std::io::Result { 10 | let mut reader = std::io::Cursor::new(slice); 11 | S::decode(&mut reader) 12 | } 13 | } 14 | 15 | pub trait Encode { 16 | fn encode(&self, writer: &mut T) -> std::io::Result<()>; 17 | 18 | fn encode_vec(&self) -> std::io::Result> { 19 | let mut v = Vec::with_capacity(self.len()); 20 | self.encode(&mut v)?; 21 | Ok(v) 22 | } 23 | 24 | fn len(&self) -> usize; 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use crate::{Decode, Encode}; 30 | 31 | #[derive(Clone, Debug, Eq, PartialEq, Default)] 32 | pub struct Dummy {} 33 | 34 | impl Encode for Dummy { 35 | fn encode(&self, _writer: &mut T) -> std::io::Result<()> { 36 | Ok(()) 37 | } 38 | 39 | fn len(&self) -> usize { 40 | 0 41 | } 42 | } 43 | 44 | impl Decode for Dummy { 45 | fn decode(_reader: &mut T) -> std::io::Result { 46 | Ok(Self {}) 47 | } 48 | } 49 | 50 | /*#[test] 51 | fn test_asn1_decode() { 52 | use serde::{Serialize, Deserialize}; 53 | use std::option::Option; 54 | 55 | let plain = Option::None; 56 | let serialized = picky_asn1_der::to_vec(&plain).unwrap(); 57 | println!("{:?}", serialized); 58 | //let deserialized: Option::None = picky_asn1_der::from_bytes(&serialized).unwrap(); 59 | }*/ 60 | } 61 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use bacnet::application::*; 2 | use bacnet::network::*; 3 | use bacnet::transport::bacnetip::*; 4 | use bacnet::{Decode, Encode}; 5 | 6 | use async_std::net::UdpSocket; 7 | use async_std::task; 8 | 9 | use tracing::trace; 10 | 11 | fn main() { 12 | tracing_subscriber::fmt::init(); 13 | 14 | task::block_on(async { 15 | let socket = UdpSocket::bind(format!("0.0.0.0:{}", 0xBAC0)) 16 | .await 17 | .unwrap(); 18 | socket.set_broadcast(true).unwrap(); 19 | let mut buf = vec![0u8; 1024]; 20 | 21 | println!("Listening on {}", socket.local_addr().unwrap()); 22 | 23 | let addr = format!("192.168.69.255:{}", 0xBAC0); 24 | let data_ref = hex::decode("810b000c0120ffff00ff1008").unwrap(); // Who-is 25 | let apdu = APDU::new(0x01, 0x08, vec![]); 26 | trace!("APDU Len: {}", apdu.len()); 27 | let dest = NPDUDest::new(0xffff, 0); 28 | let npdu = NPDU::new(apdu, Some(dest), None, NPDUPriority::Normal); 29 | let bvlc = BVLC::new(BVLCFunction::OriginalBroadcastNPDU(npdu)); 30 | let data = bvlc.encode_vec().unwrap(); 31 | println!("Who-Is: {:?}", bvlc); 32 | println!("Send: {:02x?}", data.to_vec()); 33 | println!("Ref : {:02x?}", data_ref); 34 | let sent = socket.send_to(&data, &addr).await.unwrap(); 35 | println!("Sent {} bytes to {}", sent, addr); 36 | 37 | loop { 38 | let (n, peer) = socket.recv_from(&mut buf).await.unwrap(); 39 | // === Data Structure === 40 | trace!("Data: {:02x?}", data); 41 | 42 | let b = BVLC::decode_slice(&data).unwrap(); 43 | trace!("BVLC: {:02x?}", b); 44 | trace!("Function: {:02x?}", b.function); 45 | trace!("Length: {:?}", b.len()); 46 | 47 | match b.function { 48 | BVLCFunction::OriginalBroadcastNPDU(n) | BVLCFunction::OriginalUnicastNPDU(n) => { 49 | trace!("NPDU: {:02x?}", n); 50 | trace!("Version: {}", n.version); 51 | trace!("Priority: {:?}", n.priority); 52 | match n.content { 53 | NPDUContent::APDU(apdu) => { 54 | trace!("APDU: {:02x?}", apdu); 55 | match apdu.service_choice { 56 | 08 => { 57 | trace!("Who-Is received!"); 58 | //let apdu = APDU::new(); 59 | //let sent = socket.send_to().await.unwrap(); 60 | } 61 | 00 => { 62 | trace!("I-Am received!"); 63 | } 64 | _ => unimplemented!(), 65 | } 66 | } 67 | _ => unimplemented!(), 68 | } 69 | } 70 | } 71 | } 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/network.rs: -------------------------------------------------------------------------------- 1 | use crate::application::*; 2 | use crate::{Decode, Encode}; 3 | 4 | use num_derive::{FromPrimitive, ToPrimitive}; 5 | use num_traits::{FromPrimitive, ToPrimitive}; 6 | use std::convert::TryFrom; 7 | 8 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 9 | use bytes::BufMut; 10 | 11 | use tracing::trace; 12 | 13 | /// Network Layer PDU Message Priority (6.2.2) 14 | #[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] 15 | pub enum NPDUPriority { 16 | LifeSafety = 0b11, 17 | CriticalEquipment = 0b10, 18 | Urgent = 0b01, 19 | Normal = 0b00, 20 | } 21 | 22 | impl Into for NPDUPriority { 23 | fn into(self) -> u8 { 24 | match self { 25 | Self::LifeSafety => 0b11, 26 | Self::CriticalEquipment => 0b10, 27 | Self::Urgent => 0b01, 28 | Self::Normal => 0b00, 29 | } 30 | } 31 | } 32 | 33 | impl Default for NPDUPriority { 34 | fn default() -> Self { 35 | Self::Normal 36 | } 37 | } 38 | 39 | /// Network Layer PDU Message Type (6.2.4) 40 | #[derive(Clone, Debug, Eq, PartialEq)] 41 | pub enum NPDUMessage { 42 | WhoIsRouterToNetwork, // = 0x00, 43 | IAmRouterToNetwork, // = 0x01, 44 | ICouldBeRouterToNetwork, // = 0x02, 45 | RejectMessageToNetwork, // = 0x03, 46 | RouterBusyToNetwork, // = 0x04, 47 | RouterAvailableToNetwork, // = 0x05, 48 | InitializeRoutingTable, // = 0x06, 49 | InitializeRoutingTableAck, // = 0x07, 50 | EstablishConnectionToNetwork, // = 0x08, 51 | DisconnectConnectionToNetwork, // = 0x09, 52 | ChallengeRequest, // = 0x0A, 53 | SecurityPayload, // = 0x0B, 54 | SecurityResponse, // = 0x0C, 55 | RequestKeyUpdate, // = 0x0D, 56 | UpdateKeySet, // = 0x0E, 57 | UpdateDistributionKey, // = 0x0F, 58 | RequestMasterKey, // = 0x10, 59 | SetMasterKey, // = 0x11, 60 | WhatIsNetworkNumber, // = 0x12, 61 | NetworkNumberIs, // = 0x13, 62 | Proprietary(u8), // = 0x80 to 0xFF, Available for vendor proprietary messages 63 | Reserved(u8), // = 0x14 to 0x7F, Reserved for use by ASHRAE 64 | } 65 | 66 | impl TryFrom for NPDUMessage { 67 | type Error = String; 68 | 69 | fn try_from(v: u8) -> Result { 70 | match v { 71 | 0x00 => Ok(Self::WhoIsRouterToNetwork), 72 | // TODO: Implement rest 73 | v if (v >= 0x80 && v <= 0xFF) => Ok(Self::Proprietary(v)), 74 | v => Err(format!("Unknown Message type: {}", v)), 75 | } 76 | } 77 | } 78 | 79 | impl Encode for NPDUMessage { 80 | fn encode(&self, writer: &mut T) -> std::io::Result<()> { 81 | unimplemented!(); 82 | } 83 | 84 | fn len(&self) -> usize { 85 | unimplemented!(); 86 | } 87 | } 88 | 89 | #[derive(Clone, Debug, Eq, PartialEq)] 90 | pub struct NPDUDest { 91 | net: u16, 92 | adr: Vec, 93 | hops: u8, 94 | } 95 | 96 | impl NPDUDest { 97 | pub fn new(net: u16, capacity: usize) -> Self { 98 | NPDUDest { 99 | net, 100 | adr: Vec::with_capacity(capacity), 101 | hops: 255, 102 | } 103 | } 104 | } 105 | 106 | #[derive(Clone, Debug, Eq, PartialEq, Default)] 107 | pub struct NPDUSource { 108 | net: u16, 109 | adr: Vec, 110 | } 111 | 112 | impl NPDUSource { 113 | pub fn new(net: u16, capacity: usize) -> Self { 114 | NPDUSource { 115 | net, 116 | adr: Vec::with_capacity(capacity), 117 | } 118 | } 119 | } 120 | 121 | #[derive(Clone, Debug, Eq, PartialEq)] 122 | pub enum NPDUContent { 123 | APDU(A), 124 | Message(B), 125 | } 126 | 127 | impl From for NPDUContent { 128 | fn from(apdu: A) -> Self { 129 | NPDUContent::APDU(apdu) 130 | } 131 | } 132 | 133 | impl Encode for NPDUContent { 134 | fn encode(&self, writer: &mut T) -> std::io::Result<()> { 135 | Ok(match self { 136 | Self::APDU(apdu) => apdu.encode(writer)?, 137 | Self::Message(msg) => msg.encode(writer)?, 138 | }) 139 | } 140 | 141 | fn len(&self) -> usize { 142 | match self { 143 | Self::APDU(apdu) => apdu.len(), 144 | Self::Message(msg) => msg.len(), 145 | } 146 | } 147 | } 148 | 149 | #[derive(Clone, Debug, Eq, PartialEq)] 150 | pub struct NPDU { 151 | /// Protocol Version Number (6.2.1) 152 | pub version: u8, 153 | pub destination: Option, 154 | pub source: Option, 155 | pub data_expecting_reply: bool, 156 | pub priority: NPDUPriority, 157 | pub content: NPDUContent, 158 | } 159 | 160 | impl NPDU { 161 | pub fn new>>( 162 | content: T, 163 | destination: Option, 164 | source: Option, 165 | priority: NPDUPriority, 166 | ) -> Self { 167 | NPDU { 168 | version: 1, 169 | content: content.into(), 170 | destination, 171 | source, 172 | data_expecting_reply: false, 173 | priority, 174 | } 175 | } 176 | } 177 | 178 | impl Encode for NPDU { 179 | fn encode(&self, writer: &mut T) -> std::io::Result<()> { 180 | // NPCI 181 | writer.write_u8(self.version)?; 182 | 183 | let mut control: u8 = self.priority.into(); 184 | if self.data_expecting_reply { 185 | control |= 1 << 2; 186 | } 187 | if self.source.is_some() { 188 | control |= 1 << 3; 189 | } 190 | if self.destination.is_some() { 191 | control |= 1 << 5; 192 | } 193 | if let NPDUContent::Message(_) = self.content { 194 | control |= 1 << 7; 195 | } 196 | writer.write_u8(control)?; 197 | if let Some(ref d) = self.destination { 198 | writer.write_u16::(d.net)?; 199 | writer.write_u8(d.adr.len() as u8)?; 200 | writer.write(&d.adr)?; 201 | } 202 | if let Some(ref s) = self.source { 203 | writer.write_u16::(s.net)?; 204 | writer.write_u8(s.adr.len() as u8)?; 205 | writer.write(&s.adr)?; 206 | } 207 | if let Some(ref d) = self.destination { 208 | writer.write_u8(d.hops)?; 209 | } 210 | 211 | // Content 212 | self.content.encode(writer)?; 213 | 214 | Ok(()) 215 | } 216 | 217 | fn len(&self) -> usize { 218 | let mut l: usize = 0; 219 | l += 1; // Version 220 | l += 1; // Control 221 | l += self 222 | .destination 223 | .as_ref() 224 | .and_then(|d| Some(2 + 1 + d.adr.len() + 1)) 225 | .unwrap_or(0) as usize; // DNET(2) + DLEN(1) + DADR(*) + HOPS(1) 226 | l += self 227 | .source 228 | .as_ref() 229 | .and_then(|s| Some(2 + 1 + s.adr.len())) 230 | .unwrap_or(0) as usize; // SNET(2) + SLEN(1) + SADR(*) 231 | l += self.content.len(); 232 | l 233 | } 234 | } 235 | 236 | impl Decode for NPDU { 237 | fn decode(reader: &mut T) -> std::io::Result { 238 | let version = reader.read_u8()?; 239 | trace!("Version: {:02x}", version); 240 | // Read and parse the Network Layer Protocol Control Information (6.2.2) 241 | let control = reader.read_u8()?; 242 | trace!("Control: {:08b}", control); 243 | let priority = NPDUPriority::from_u8(control & 0b0000_00011).unwrap(); 244 | let has_apdu = (control & 1 << 7) == 0; 245 | let has_dest = (control & 1 << 5) != 0; 246 | let has_source = (control & 1 << 3) != 0; 247 | let data_expecting_reply = (control & 1 << 2) != 0; 248 | 249 | let mut destination: Option = if has_dest { 250 | let net = reader.read_u16::()?; 251 | let len = reader.read_u8()?; 252 | let mut dest = NPDUDest::new(net, len as usize); 253 | reader.read_exact(&mut dest.adr)?; 254 | Some(dest) 255 | } else { 256 | None 257 | }; 258 | 259 | let source: Option = if has_source { 260 | let net = reader.read_u16::()?; 261 | let len = reader.read_u8()?; 262 | let mut source = NPDUSource::new(net, len as usize); 263 | reader.read_exact(&mut source.adr)?; 264 | Some(source) 265 | } else { 266 | None 267 | }; 268 | println!("{:?}", destination); 269 | if let Some(dest) = &mut destination { 270 | dest.hops = reader.read_u8()?; 271 | }; 272 | 273 | let content = if has_apdu { 274 | APDU::decode(reader)?.into() 275 | } else { 276 | /*Ok(NPDUContentSlice::Message(NPDUMessage::try_from( 277 | self.slice[0], 278 | )?))*/ 279 | unimplemented!(); 280 | }; 281 | 282 | Ok(Self { 283 | version, 284 | destination, 285 | source, 286 | data_expecting_reply, 287 | priority, 288 | content, 289 | }) 290 | } 291 | } 292 | 293 | #[cfg(test)] 294 | mod tests { 295 | use super::*; 296 | use crate::{Decode, Encode}; 297 | use bytes::{BufMut, BytesMut}; 298 | 299 | use crate::tests::*; 300 | 301 | #[test] 302 | fn test_encode_npdu() { 303 | let content = NPDUContent::::APDU(Dummy::default()); 304 | let npdu = NPDU::::new(content, None, None, NPDUPriority::Normal); 305 | 306 | let mut w = BytesMut::new().writer(); 307 | npdu.encode(&mut w).expect("Write NPDU to buffer"); 308 | assert_eq!(w.into_inner().to_vec(), vec![1, 0]); 309 | } 310 | 311 | #[test] 312 | fn test_encode_npdu_with_dest() { 313 | let content = NPDUContent::::APDU(Dummy::default()); 314 | let dest = NPDUDest { 315 | net: 0x126, 316 | adr: vec![0; 16], 317 | hops: 255, 318 | }; 319 | let npdu = NPDU::::new(content, Some(dest), None, NPDUPriority::Normal); 320 | 321 | let mut w = BytesMut::new().writer(); 322 | npdu.encode(&mut w).expect("Write NPDU to buffer"); 323 | assert_eq!( 324 | w.into_inner().to_vec(), 325 | vec![1, 32, 1, 38, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255] 326 | ); 327 | } 328 | 329 | #[test] 330 | fn test_encode_npdu_with_source() { 331 | let content = NPDUContent::::APDU(Dummy::default()); 332 | let source = NPDUSource { 333 | net: 0x126, 334 | adr: vec![0; 16], 335 | }; 336 | let npdu = NPDU::::new(content, None, Some(source), NPDUPriority::Normal); 337 | 338 | let mut w = BytesMut::new().writer(); 339 | npdu.encode(&mut w).expect("Write NPDU to buffer"); 340 | assert_eq!( 341 | w.into_inner().to_vec(), 342 | vec![1, 8, 1, 38, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 343 | ); 344 | } 345 | 346 | #[test] 347 | fn test_encode_npdu_with_dest_and_source() { 348 | let content = NPDUContent::::APDU(Dummy::default()); 349 | let dest = NPDUDest { 350 | net: 0x126, 351 | adr: vec![0; 16], 352 | hops: 255, 353 | }; 354 | let source = NPDUSource { 355 | net: 0x126, 356 | adr: vec![0; 16], 357 | }; 358 | let npdu = 359 | NPDU::::new(content, Some(dest), Some(source), NPDUPriority::Normal); 360 | 361 | let mut w = BytesMut::with_capacity(1024).writer(); 362 | npdu.encode(&mut w).expect("Write NPDU to buffer"); 363 | assert_eq!( 364 | w.into_inner().to_vec(), 365 | vec![ 366 | 1, 40, 1, 38, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 38, 16, 0, 0, 367 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255 368 | ] 369 | ); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/transport.rs: -------------------------------------------------------------------------------- 1 | /// Implement of the data link and physical layer 2 | /// 3 | /// - [ ] Ethernet (ISO 8802-3) Clause 7 4 | /// - [ ] ARCNET (ATA 878.1) Clause 8 5 | /// - [ ] MS/TP Clause 9 6 | /// - [ ] PTP Clause 10 7 | /// - [ ] LonTalk (ISO/IEC 14908.1) Clause 11 8 | /// - [x] BACnet/IP Annex J 9 | /// - [ ] BACnet/IPv6 Annex U 10 | /// - [ ] ZigBee Annex O 11 | /// - [ ] BACnet/SC Annex AB 12 | /// 13 | /// See Figure 4-2. BACnet collapsed architecture. 14 | /// 15 | /// 16 | pub mod bacnetip; 17 | pub mod bacnetsc; 18 | -------------------------------------------------------------------------------- /src/transport/bacnetip.rs: -------------------------------------------------------------------------------- 1 | /// Implements BACnet/IP (Annex J) 2 | use crate::network::*; 3 | use crate::{Decode, Encode}; 4 | 5 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 6 | use bytes::BufMut; 7 | 8 | const BACNETIP: u8 = 0x81; 9 | 10 | pub trait AsU8 { 11 | fn as_u8(&self) -> u8; 12 | } 13 | 14 | /// BACnet Virtual Link Control Function 15 | #[derive(Clone, Debug, Eq, PartialEq)] 16 | pub enum BVLCFunction { 17 | OriginalBroadcastNPDU(NPDU), 18 | OriginalUnicastNPDU(NPDU), 19 | } 20 | 21 | impl AsU8 for BVLCFunction { 22 | fn as_u8(&self) -> u8 { 23 | match self { 24 | Self::OriginalBroadcastNPDU(_) => 0x0b, 25 | Self::OriginalUnicastNPDU(_) => 0x0a, 26 | } 27 | } 28 | } 29 | 30 | impl Encode for BVLCFunction { 31 | fn encode(&self, writer: &mut T) -> std::io::Result<()> { 32 | match self { 33 | Self::OriginalBroadcastNPDU(n) | Self::OriginalUnicastNPDU(n) => n.encode(writer)?, 34 | } 35 | Ok(()) 36 | } 37 | 38 | fn len(&self) -> usize { 39 | match self { 40 | Self::OriginalBroadcastNPDU(n) | Self::OriginalUnicastNPDU(n) => n.len(), 41 | } 42 | } 43 | } 44 | 45 | /// A Struct containing a BACnet Virtual Link Control (Annex J). 46 | #[derive(Clone, Debug, Eq, PartialEq)] 47 | pub struct BVLC { 48 | bvlc_type: u8, 49 | pub function: F, 50 | } 51 | 52 | impl BVLC { 53 | pub fn new(function: F) -> Self { 54 | Self { 55 | bvlc_type: BACNETIP, // BACnet/IP (Annex J) 56 | function: function, 57 | } 58 | } 59 | 60 | pub fn set_function(&mut self, function: F) { 61 | self.function = function; 62 | } 63 | } 64 | 65 | impl Encode for BVLC { 66 | fn encode(&self, writer: &mut T) -> std::io::Result<()> { 67 | writer.write_u8(self.bvlc_type)?; 68 | writer.write_u8(self.function.as_u8())?; 69 | writer.write_u16::(self.len() as u16)?; 70 | self.function.encode(writer)?; 71 | Ok(()) 72 | } 73 | 74 | fn len(&self) -> usize { 75 | let mut l: usize = 0; 76 | l += 1; // Type 77 | l += 1; // Function 78 | l += 2; // Content Length 79 | l += self.function.len(); // Function Content 80 | l 81 | } 82 | } 83 | 84 | impl Decode for BVLC { 85 | fn decode(reader: &mut T) -> std::io::Result { 86 | let bvlc_type = reader.read_u8()?; 87 | if bvlc_type != BACNETIP { 88 | return Err(std::io::Error::new( 89 | std::io::ErrorKind::InvalidData, 90 | format!("BVLC type not supported: {}", bvlc_type), 91 | )); 92 | } 93 | let function = reader.read_u8()?; 94 | let lenght = reader.read_u16::()?; // TODO: Check lenght 95 | let function = match function { 96 | 0x0b => { 97 | let npdu = NPDU::decode(reader)?; 98 | Ok(BVLCFunction::OriginalBroadcastNPDU(npdu)) 99 | } 100 | 0x0a => { 101 | let npdu = NPDU::decode(reader)?; 102 | Ok(BVLCFunction::OriginalUnicastNPDU(npdu)) 103 | } 104 | t => Err(std::io::Error::new( 105 | std::io::ErrorKind::InvalidData, 106 | format!("BVLC Function not supported: {}", t), 107 | )), 108 | }; 109 | Ok(Self::new(function?)) 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use super::*; 116 | use crate::{Decode, Encode}; 117 | use bytes::{BufMut, BytesMut}; 118 | use hex; 119 | 120 | use crate::tests::*; 121 | 122 | impl AsU8 for Dummy { 123 | fn as_u8(&self) -> u8 { 124 | 0x00 125 | } 126 | } 127 | 128 | #[test] 129 | fn test_encode_bvlc() { 130 | let bvlc = BVLC::::new(Dummy::default()); 131 | 132 | let mut w = BytesMut::new().writer(); 133 | bvlc.encode(&mut w).expect("Write BVLC to buffer"); 134 | assert_eq!(w.into_inner().to_vec(), vec![129, 0, 0, 4]); 135 | } 136 | 137 | #[test] 138 | fn test_decode_invalid_bvlc_type() { 139 | let data = hex::decode("00000000").unwrap(); 140 | let err = BVLC::decode(&mut std::io::Cursor::new(&data)).unwrap_err(); 141 | 142 | assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); 143 | assert_eq!( 144 | err.into_inner().unwrap().to_string(), 145 | "BVLC type not supported: 0".to_string() 146 | ); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/transport/bacnetsc.rs: -------------------------------------------------------------------------------- 1 | /// Implements BACnet/SC (Annex YY) 2 | use crate::network::*; 3 | use crate::{Decode, Encode}; 4 | 5 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 6 | 7 | const BACNETSC: u8 = 0x81; 8 | --------------------------------------------------------------------------------