├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── assets ├── qr_text_12mm.png ├── qr_text_18mm.png ├── qr_text_24mm.png ├── qr_text_36mm.png ├── qr_text_4mm.png ├── qr_text_6mm.png ├── qr_text_9mm.png ├── test_pattern_12mm.png ├── test_pattern_18mm.png ├── test_pattern_24mm.png ├── test_pattern_36mm.png ├── test_pattern_4mm.png ├── test_pattern_6mm.png └── test_pattern_9mm.png ├── monitor_and_print_mac.sh ├── rust-toolchain └── src ├── analyzer.rs ├── display.rs ├── lib.rs ├── main.rs ├── print.rs └── protocol.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.pcapng 3 | *.swp 4 | *.bmp 5 | /*.png 6 | *.txt 7 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "adler32" 13 | version = "1.2.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "0.7.20" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "anyhow" 28 | version = "1.0.66" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" 31 | 32 | [[package]] 33 | name = "argh" 34 | version = "0.1.9" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "c375edecfd2074d5edcc31396860b6e54b6f928714d0e097b983053fac0cabe3" 37 | dependencies = [ 38 | "argh_derive", 39 | "argh_shared", 40 | ] 41 | 42 | [[package]] 43 | name = "argh_derive" 44 | version = "0.1.9" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "aa013479b80109a1bf01a039412b0f0013d716f36921226d86c6709032fb7a03" 47 | dependencies = [ 48 | "argh_shared", 49 | "heck", 50 | "proc-macro2", 51 | "quote", 52 | "syn", 53 | ] 54 | 55 | [[package]] 56 | name = "argh_shared" 57 | version = "0.1.9" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "149f75bbec1827618262e0855a68f0f9a7f2edc13faebf33c4f16d6725edb6a9" 60 | 61 | [[package]] 62 | name = "autocfg" 63 | version = "1.1.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 66 | 67 | [[package]] 68 | name = "az" 69 | version = "1.2.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" 72 | 73 | [[package]] 74 | name = "barcoders" 75 | version = "1.0.2" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "298326aba91e0dd1acba62767652c7c9f4d45ada6804110278680c4f0c6e8ab7" 78 | 79 | [[package]] 80 | name = "bitflags" 81 | version = "1.3.2" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 84 | 85 | [[package]] 86 | name = "bytemuck" 87 | version = "1.12.3" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" 90 | 91 | [[package]] 92 | name = "byteorder" 93 | version = "1.4.3" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 96 | 97 | [[package]] 98 | name = "cfg-if" 99 | version = "1.0.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 102 | 103 | [[package]] 104 | name = "checked_int_cast" 105 | version = "1.0.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" 108 | 109 | [[package]] 110 | name = "color_quant" 111 | version = "1.1.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 114 | 115 | [[package]] 116 | name = "crc32fast" 117 | version = "1.3.2" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 120 | dependencies = [ 121 | "cfg-if", 122 | ] 123 | 124 | [[package]] 125 | name = "crossbeam-channel" 126 | version = "0.5.6" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" 129 | dependencies = [ 130 | "cfg-if", 131 | "crossbeam-utils", 132 | ] 133 | 134 | [[package]] 135 | name = "crossbeam-deque" 136 | version = "0.8.2" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" 139 | dependencies = [ 140 | "cfg-if", 141 | "crossbeam-epoch", 142 | "crossbeam-utils", 143 | ] 144 | 145 | [[package]] 146 | name = "crossbeam-epoch" 147 | version = "0.9.13" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" 150 | dependencies = [ 151 | "autocfg", 152 | "cfg-if", 153 | "crossbeam-utils", 154 | "memoffset", 155 | "scopeguard", 156 | ] 157 | 158 | [[package]] 159 | name = "crossbeam-utils" 160 | version = "0.8.14" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" 163 | dependencies = [ 164 | "cfg-if", 165 | ] 166 | 167 | [[package]] 168 | name = "deflate" 169 | version = "0.8.6" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" 172 | dependencies = [ 173 | "adler32", 174 | "byteorder", 175 | ] 176 | 177 | [[package]] 178 | name = "either" 179 | version = "1.8.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" 182 | 183 | [[package]] 184 | name = "embedded-graphics" 185 | version = "0.7.1" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "750082c65094fbcc4baf9ba31583ce9a8bb7f52cadfb96f6164b1bc7f922f32b" 188 | dependencies = [ 189 | "az", 190 | "byteorder", 191 | "embedded-graphics-core", 192 | "float-cmp", 193 | "micromath", 194 | ] 195 | 196 | [[package]] 197 | name = "embedded-graphics-core" 198 | version = "0.3.3" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "b8b1239db5f3eeb7e33e35bd10bd014e7b2537b17e071f726a09351431337cfa" 201 | dependencies = [ 202 | "az", 203 | "byteorder", 204 | ] 205 | 206 | [[package]] 207 | name = "flate2" 208 | version = "1.0.25" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" 211 | dependencies = [ 212 | "crc32fast", 213 | "miniz_oxide 0.6.2", 214 | ] 215 | 216 | [[package]] 217 | name = "float-cmp" 218 | version = "0.8.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" 221 | dependencies = [ 222 | "num-traits", 223 | ] 224 | 225 | [[package]] 226 | name = "gif" 227 | version = "0.11.4" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" 230 | dependencies = [ 231 | "color_quant", 232 | "weezl", 233 | ] 234 | 235 | [[package]] 236 | name = "heck" 237 | version = "0.4.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 240 | 241 | [[package]] 242 | name = "hermit-abi" 243 | version = "0.1.19" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 246 | dependencies = [ 247 | "libc", 248 | ] 249 | 250 | [[package]] 251 | name = "image" 252 | version = "0.23.14" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" 255 | dependencies = [ 256 | "bytemuck", 257 | "byteorder", 258 | "color_quant", 259 | "gif", 260 | "jpeg-decoder", 261 | "num-iter", 262 | "num-rational", 263 | "num-traits", 264 | "png 0.16.8", 265 | "scoped_threadpool", 266 | "tiff", 267 | ] 268 | 269 | [[package]] 270 | name = "jpeg-decoder" 271 | version = "0.1.22" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" 274 | dependencies = [ 275 | "rayon", 276 | ] 277 | 278 | [[package]] 279 | name = "libc" 280 | version = "0.2.138" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" 283 | 284 | [[package]] 285 | name = "memchr" 286 | version = "2.5.0" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 289 | 290 | [[package]] 291 | name = "memoffset" 292 | version = "0.7.1" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 295 | dependencies = [ 296 | "autocfg", 297 | ] 298 | 299 | [[package]] 300 | name = "micromath" 301 | version = "1.1.1" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "bc4010833aea396656c2f91ee704d51a6f1329ec2ab56ffd00bfd56f7481ea94" 304 | 305 | [[package]] 306 | name = "miniz_oxide" 307 | version = "0.3.7" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" 310 | dependencies = [ 311 | "adler32", 312 | ] 313 | 314 | [[package]] 315 | name = "miniz_oxide" 316 | version = "0.4.4" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" 319 | dependencies = [ 320 | "adler", 321 | "autocfg", 322 | ] 323 | 324 | [[package]] 325 | name = "miniz_oxide" 326 | version = "0.6.2" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 329 | dependencies = [ 330 | "adler", 331 | ] 332 | 333 | [[package]] 334 | name = "num-integer" 335 | version = "0.1.45" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 338 | dependencies = [ 339 | "autocfg", 340 | "num-traits", 341 | ] 342 | 343 | [[package]] 344 | name = "num-iter" 345 | version = "0.1.43" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" 348 | dependencies = [ 349 | "autocfg", 350 | "num-integer", 351 | "num-traits", 352 | ] 353 | 354 | [[package]] 355 | name = "num-rational" 356 | version = "0.3.2" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" 359 | dependencies = [ 360 | "autocfg", 361 | "num-integer", 362 | "num-traits", 363 | ] 364 | 365 | [[package]] 366 | name = "num-traits" 367 | version = "0.2.15" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 370 | dependencies = [ 371 | "autocfg", 372 | ] 373 | 374 | [[package]] 375 | name = "num_cpus" 376 | version = "1.14.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" 379 | dependencies = [ 380 | "hermit-abi", 381 | "libc", 382 | ] 383 | 384 | [[package]] 385 | name = "png" 386 | version = "0.16.8" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" 389 | dependencies = [ 390 | "bitflags", 391 | "crc32fast", 392 | "deflate", 393 | "miniz_oxide 0.3.7", 394 | ] 395 | 396 | [[package]] 397 | name = "png" 398 | version = "0.17.7" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" 401 | dependencies = [ 402 | "bitflags", 403 | "crc32fast", 404 | "flate2", 405 | "miniz_oxide 0.6.2", 406 | ] 407 | 408 | [[package]] 409 | name = "proc-macro2" 410 | version = "1.0.70" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 413 | dependencies = [ 414 | "unicode-ident", 415 | ] 416 | 417 | [[package]] 418 | name = "qrcode" 419 | version = "0.12.0" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" 422 | dependencies = [ 423 | "checked_int_cast", 424 | "image", 425 | ] 426 | 427 | [[package]] 428 | name = "quote" 429 | version = "1.0.21" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 432 | dependencies = [ 433 | "proc-macro2", 434 | ] 435 | 436 | [[package]] 437 | name = "rayon" 438 | version = "1.6.1" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" 441 | dependencies = [ 442 | "either", 443 | "rayon-core", 444 | ] 445 | 446 | [[package]] 447 | name = "rayon-core" 448 | version = "1.10.1" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" 451 | dependencies = [ 452 | "crossbeam-channel", 453 | "crossbeam-deque", 454 | "crossbeam-utils", 455 | "num_cpus", 456 | ] 457 | 458 | [[package]] 459 | name = "regex" 460 | version = "1.7.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" 463 | dependencies = [ 464 | "aho-corasick", 465 | "memchr", 466 | "regex-syntax", 467 | ] 468 | 469 | [[package]] 470 | name = "regex-syntax" 471 | version = "0.6.28" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 474 | 475 | [[package]] 476 | name = "scoped_threadpool" 477 | version = "0.1.9" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" 480 | 481 | [[package]] 482 | name = "scopeguard" 483 | version = "1.1.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 486 | 487 | [[package]] 488 | name = "sr5900p" 489 | version = "0.1.0" 490 | dependencies = [ 491 | "anyhow", 492 | "argh", 493 | "barcoders", 494 | "embedded-graphics", 495 | "image", 496 | "png 0.17.7", 497 | "proc-macro2", 498 | "qrcode", 499 | "regex", 500 | ] 501 | 502 | [[package]] 503 | name = "syn" 504 | version = "1.0.103" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" 507 | dependencies = [ 508 | "proc-macro2", 509 | "quote", 510 | "unicode-ident", 511 | ] 512 | 513 | [[package]] 514 | name = "tiff" 515 | version = "0.6.1" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" 518 | dependencies = [ 519 | "jpeg-decoder", 520 | "miniz_oxide 0.4.4", 521 | "weezl", 522 | ] 523 | 524 | [[package]] 525 | name = "unicode-ident" 526 | version = "1.0.5" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 529 | 530 | [[package]] 531 | name = "weezl" 532 | version = "0.1.7" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" 535 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sr5900p" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | argh = "0.1.9" 8 | anyhow = "1.0.66" 9 | barcoders = "1.0.2" 10 | embedded-graphics = "0.7.1" 11 | png = "~0.17.7" 12 | qrcode = "0.12.0" 13 | image = "^0.23" 14 | regex = "1" 15 | proc-macro2 = "1.0.70" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 hikalium 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | cargo build --release 3 | 4 | clippy: 5 | cargo clippy -- -D warnings 6 | 7 | TAPE_TO_TEST=4 6 9 12 18 24 36 8 | 9 | test: 10 | for w in $(TAPE_TO_TEST) ; do \ 11 | cargo run --release -- print --test-pattern --width $${w} --dry-run && \ 12 | diff preview.png assets/test_pattern_$${w}mm.png && \ 13 | echo OK || { echo "FAIL: test_pattern $${w}mm" ; exit 1 ; } \ 14 | done 15 | for w in $(TAPE_TO_TEST) ; do \ 16 | cargo run --release -- print --qr-text "Hello, world!" --width $${w} --dry-run && \ 17 | diff preview.png assets/qr_text_$${w}mm.png && \ 18 | echo OK || { echo "FAIL: qr_text $${w}mm" ; exit 1 ; } \ 19 | done 20 | 21 | commit: clippy test 22 | 23 | run: 24 | ifndef PRINTER_IP 25 | $(error Please set PRINTER_IP) 26 | endif 27 | cargo run -- print --printer ${PRINTER_IP} --test-pattern 28 | 29 | build_static: 30 | RUSTFLAGS='-C target-feature=+crt-static' cargo build --target x86_64-unknown-linux-gnu 31 | 32 | install: 33 | cargo install --path . 34 | 35 | analyze: 36 | cargo run -- analyze --tcp-data sample_tcp_data/w18_Aaa.bin 37 | 38 | print: 39 | cargo run -- print --printer 10.10.10.31 --tcp-data sample_tcp_data/w18_hikalium.bin 40 | 41 | analyze_all: 42 | find ./sample_tcp_data/*.bin | xargs -I {} -- bash -c 'echo "*** {}" && cargo run -q -- analyze --tcp-data {} | grep -v "cmd 0x1b 0x2e"' 43 | 44 | gen: 45 | cargo run -- print --printer 10.10.10.31 --gen-test --dry-run 46 | 47 | gen_print: 48 | cargo run -- print --printer 10.10.10.31 --gen-test 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sr5900p 2 | 3 | A command-line interface for SR5900P tape printer. 4 | 5 | ![](./assets/test_pattern_18mm.png) 6 | ![](./assets/qr_text_18mm.png) 7 | 8 | ``` 9 | # Install 10 | make install 11 | 12 | # Preview - preview.png will be generated after each commands: 13 | sr5900p print --dry-run --width 36 --test-pattern 14 | sr5900p print --dry-run --width 12 --test-pattern 15 | sr5900p print --dry-run --width 12 --qr-text 'Hello, world!' 16 | sr5900p print --dry-run --printer ${PRINTER_IP} --qr-text 'Hello, world!' # width auto detect 17 | 18 | # Detect your printer's IP with avahi-browse: 19 | sudo apt-get install -y avahi-utils 20 | PRINTER_IP=`avahi-browse -alrpt | grep -E '^=.*SR5900P' | cut -d ';' -f 8` 21 | 22 | # OR, you can manually set the IP: 23 | PRINTER_IP=${YOUR_PRINTER_IP} 24 | 25 | # Let's print! 26 | sr5900p print --printer ${PRINTER_IP} --test-pattern 27 | sr5900p print --printer ${PRINTER_IP} --qr-text 'Hello, world!' 28 | ``` 29 | 30 | ## License 31 | MIT 32 | 33 | ## Author 34 | hikalium 35 | 36 | ## Special Thanks 37 | Mine02C4 (for [the initial analysis of the protocol](https://github.com/Mine02C4/TEPRA_PRO_SR5900P_analysis) ) 38 | -------------------------------------------------------------------------------- /assets/qr_text_12mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/qr_text_12mm.png -------------------------------------------------------------------------------- /assets/qr_text_18mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/qr_text_18mm.png -------------------------------------------------------------------------------- /assets/qr_text_24mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/qr_text_24mm.png -------------------------------------------------------------------------------- /assets/qr_text_36mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/qr_text_36mm.png -------------------------------------------------------------------------------- /assets/qr_text_4mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/qr_text_4mm.png -------------------------------------------------------------------------------- /assets/qr_text_6mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/qr_text_6mm.png -------------------------------------------------------------------------------- /assets/qr_text_9mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/qr_text_9mm.png -------------------------------------------------------------------------------- /assets/test_pattern_12mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/test_pattern_12mm.png -------------------------------------------------------------------------------- /assets/test_pattern_18mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/test_pattern_18mm.png -------------------------------------------------------------------------------- /assets/test_pattern_24mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/test_pattern_24mm.png -------------------------------------------------------------------------------- /assets/test_pattern_36mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/test_pattern_36mm.png -------------------------------------------------------------------------------- /assets/test_pattern_4mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/test_pattern_4mm.png -------------------------------------------------------------------------------- /assets/test_pattern_6mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/test_pattern_6mm.png -------------------------------------------------------------------------------- /assets/test_pattern_9mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hikalium/sr5900p/5069a94396df4865884c25f6d1a4f6192d2b4a4c/assets/test_pattern_9mm.png -------------------------------------------------------------------------------- /monitor_and_print_mac.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function expect_arg() { 3 | VALUE="$1" 4 | NAME="$2" 5 | if [ -z $1 ]; then 6 | echo "Expected arg ${NAME}"; 7 | exit 1; 8 | else 9 | export "${NAME}"="${VALUE}" 10 | echo "${NAME}"="${VALUE}" 11 | fi 12 | } 13 | expect_arg "$1" "PRINTER_IP" 14 | function list_mac { 15 | ifconfig | grep ether | sed -E 's/^.*ether ([a-f0-9\:]+).*$/\1/' | sort | uniq 16 | } 17 | list_mac | tee mac_block_list.txt 18 | while true 19 | do 20 | NEW_MAC_LIST=`ifconfig | grep ether | sed -E 's/^.*ether ([a-f0-9\:]+).*$/\1/' | sort | uniq | grep -v -F -f mac_block_list.txt` 21 | RETCODE=$? 22 | if [ ${RETCODE} -ne 0 ] 23 | then 24 | echo "not found" 25 | else 26 | echo "found" 27 | echo "${NEW_MAC_LIST}" | xargs -I {} -- bash -c "./sr5900p print --printer ${PRINTER_IP} --mac-addr {} && echo {} >> mac_block_list.txt" 28 | fi 29 | sleep 1 30 | done 31 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly-2022-12-01 2 | -------------------------------------------------------------------------------- /src/analyzer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use anyhow::Result; 3 | use std::num::Wrapping; 4 | 5 | pub fn analyze_tcp_data(data: &[u8]) -> Result<()> { 6 | println!("Size: {}", data.len()); 7 | let mut i = 0; 8 | let mut num_data_rows = 0; 9 | while i < data.len() { 10 | match data[i] { 11 | 0x1b => match data[i + 1] { 12 | 0x7b => { 13 | let payload_data = &data[i..i + 3 + data[i + 2] as usize]; 14 | println!("{payload_data:?}"); 15 | i += payload_data.len(); 16 | let mut payload_data = &payload_data[3..]; 17 | 18 | if payload_data.last().unwrap() != &0x7d { 19 | return Err(anyhow!( 20 | "Unexpected label data (not 0x7d): {:?}...", 21 | &data[i..i + 16] 22 | )); 23 | } 24 | payload_data.take_last(); 25 | 26 | if payload_data 27 | .iter() 28 | .map(|v| Wrapping(*v)) 29 | .sum::>() 30 | .0 31 | != payload_data.last().unwrap().wrapping_mul(2) 32 | { 33 | return Err(anyhow!( 34 | "Unexpected label data (csum invalid): {:?}...", 35 | &data[i..i + 16] 36 | )); 37 | } 38 | // so the last byte of the payload_data is the checksum 39 | payload_data.take_last(); 40 | if payload_data[0] == 76 { 41 | let mut tape_len_bytes = [0u8; 4]; 42 | tape_len_bytes.copy_from_slice(&payload_data[1..5]); 43 | let tape_len = u32::from_le_bytes(tape_len_bytes); 44 | println!("cmd 0x1b 0x7b, {payload_data:?} tape_len = {}", tape_len); 45 | } else { 46 | println!("cmd 0x1b 0x7b, {payload_data:?}"); 47 | } 48 | } 49 | 0x2e => { 50 | if data[i + 2..i + 6] != [0, 0, 0, 1] { 51 | return Err(anyhow!("Unexpected label data: {:?}...", &data[i..i + 16])); 52 | } 53 | let bits = data[i + 6] as usize + data[i + 7] as usize * 256; 54 | let bytes = (bits + 7) / 8; 55 | print!("cmd 0x1b 0x2e, bits = {bits}, bytes = {bytes}: ",); 56 | let img_data = &data[i + 8..i + 8 + bytes]; 57 | for byte in img_data { 58 | print!("{byte:08b}"); 59 | } 60 | println!(); 61 | i += 8 + bytes; 62 | num_data_rows += 1; 63 | } 64 | _ => { 65 | return Err(anyhow!("Unexpected label data: {:?}...", &data[i..i + 16])); 66 | } 67 | }, 68 | 0x0c => { 69 | println!("cmd 0x0c (data end marker?)",); 70 | i += 1; 71 | } 72 | _ => { 73 | return Err(anyhow!("Unexpected label data: {:?}...", &data[i..])); 74 | } 75 | } 76 | } 77 | println!("num_data_rows = {}", num_data_rows); 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics::pixelcolor::BinaryColor; 2 | use embedded_graphics::prelude::DrawTarget; 3 | use embedded_graphics::prelude::OriginDimensions; 4 | use embedded_graphics::prelude::Size; 5 | use embedded_graphics::Pixel; 6 | 7 | pub struct TapeDisplay { 8 | pub framebuffer: Vec>, 9 | pub width: usize, 10 | pub height: usize, 11 | } 12 | impl TapeDisplay { 13 | pub fn new(width: usize, height: usize) -> Self { 14 | let mut row = Vec::new(); 15 | row.resize(width, false); 16 | let mut framebuffer = Vec::new(); 17 | framebuffer.resize(height, row); 18 | Self { 19 | framebuffer, 20 | width, 21 | height, 22 | } 23 | } 24 | pub fn scaled(&self, r: usize) -> Self { 25 | let mut new = Self::new(self.width * r, self.height * r); 26 | for y in 0..new.height { 27 | for x in 0..new.width { 28 | new.set_pixel(x, y, self.get_pixel(x / r, y / r)); 29 | } 30 | } 31 | new 32 | } 33 | pub fn rotated(&self) -> Self { 34 | // 90 deg left (counter-clockwise) 35 | let mut new = Self::new(self.height, self.width); 36 | for y in 0..new.height { 37 | for x in 0..new.width { 38 | new.set_pixel(x, y, self.get_pixel(new.height - 1 - y, x)); 39 | } 40 | } 41 | new 42 | } 43 | pub fn overlay_or(&mut self, td: &Self, px: usize, py: usize) { 44 | for y in py..self.height { 45 | for x in px..self.width { 46 | self.framebuffer[y][x] |= td 47 | .framebuffer 48 | .get(y - py) 49 | .and_then(|r| r.get(x - px)) 50 | .unwrap_or(&false); 51 | } 52 | } 53 | } 54 | pub fn get_pixel(&self, x: usize, y: usize) -> bool { 55 | *self 56 | .framebuffer 57 | .get(y) 58 | .and_then(|r| r.get(x)) 59 | .unwrap_or(&false) 60 | } 61 | pub fn set_pixel(&mut self, x: usize, y: usize, value: bool) { 62 | if let Some(v) = self.framebuffer.get_mut(y).and_then(|r| r.get_mut(x)) { 63 | *v = value 64 | } 65 | } 66 | } 67 | impl DrawTarget for TapeDisplay { 68 | type Color = BinaryColor; 69 | type Error = core::convert::Infallible; 70 | 71 | fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> 72 | where 73 | I: IntoIterator>, 74 | { 75 | let w = self.width as i32; 76 | let h = self.height as i32; 77 | for Pixel(coord, color) in pixels.into_iter() { 78 | let (x, y) = coord.into(); 79 | if (0..w).contains(&x) && (0..h).contains(&y) { 80 | self.framebuffer[y as usize][x as usize] = color.is_on(); 81 | } 82 | } 83 | Ok(()) 84 | } 85 | } 86 | impl OriginDimensions for TapeDisplay { 87 | fn size(&self) -> Size { 88 | Size::new(self.width as u32, self.height as u32) 89 | } 90 | } 91 | 92 | #[test] 93 | fn transforms() { 94 | // 2x2 95 | let mut td = TapeDisplay::new(2, 2); 96 | // 0 0 97 | // 0 0 98 | assert_eq!(td.get_pixel(0, 0), false); 99 | assert_eq!(td.get_pixel(0, 1), false); 100 | assert_eq!(td.get_pixel(1, 0), false); 101 | assert_eq!(td.get_pixel(1, 1), false); 102 | td.set_pixel(0, 0, true); 103 | td.set_pixel(1, 1, true); 104 | // 1 0 105 | // 0 1 106 | assert_eq!(td.get_pixel(0, 0), true); 107 | assert_eq!(td.get_pixel(0, 1), false); 108 | assert_eq!(td.get_pixel(1, 0), false); 109 | assert_eq!(td.get_pixel(1, 1), true); 110 | let td = td.scaled(2); 111 | // 1 1 0 0 112 | // 1 1 0 0 113 | // 0 0 1 1 114 | // 0 0 1 1 115 | assert_eq!(td.get_pixel(0, 0), true); 116 | assert_eq!(td.get_pixel(0, 2), false); 117 | assert_eq!(td.get_pixel(2, 0), false); 118 | assert_eq!(td.get_pixel(2, 2), true); 119 | let td = td.rotated(); 120 | // 0 0 1 1 121 | // 0 0 1 1 122 | // 1 1 0 0 123 | // 1 1 0 0 124 | assert_eq!(td.get_pixel(0, 0), false); 125 | assert_eq!(td.get_pixel(0, 2), true); 126 | assert_eq!(td.get_pixel(2, 0), true); 127 | assert_eq!(td.get_pixel(2, 2), false); 128 | let td = td.rotated(); 129 | // 1 1 0 0 130 | // 1 1 0 0 131 | // 0 0 1 1 132 | // 0 0 1 1 133 | assert_eq!(td.get_pixel(0, 0), true); 134 | assert_eq!(td.get_pixel(0, 2), false); 135 | assert_eq!(td.get_pixel(2, 0), false); 136 | assert_eq!(td.get_pixel(2, 2), true); 137 | 138 | // 3x2 139 | let mut td = TapeDisplay::new(3, 2); 140 | // 0 0 0 141 | // 0 0 0 142 | assert_eq!(td.get_pixel(0, 0), false); 143 | assert_eq!(td.get_pixel(0, 1), false); 144 | assert_eq!(td.get_pixel(1, 0), false); 145 | assert_eq!(td.get_pixel(1, 1), false); 146 | assert_eq!(td.get_pixel(2, 0), false); 147 | assert_eq!(td.get_pixel(2, 1), false); 148 | td.set_pixel(0, 0, true); 149 | td.set_pixel(1, 1, true); 150 | td.set_pixel(2, 0, true); 151 | // 1 0 1 152 | // 0 1 0 153 | assert_eq!(td.get_pixel(0, 0), true); 154 | assert_eq!(td.get_pixel(0, 1), false); 155 | assert_eq!(td.get_pixel(1, 0), false); 156 | assert_eq!(td.get_pixel(1, 1), true); 157 | assert_eq!(td.get_pixel(2, 0), true); 158 | assert_eq!(td.get_pixel(2, 1), false); 159 | let td = td.scaled(2); 160 | // 1 1 0 0 1 1 161 | // 1 1 0 0 1 1 162 | // 0 0 1 1 0 0 163 | // 0 0 1 1 0 0 164 | assert_eq!(td.get_pixel(0, 0), true); 165 | assert_eq!(td.get_pixel(0, 2), false); 166 | assert_eq!(td.get_pixel(2, 0), false); 167 | assert_eq!(td.get_pixel(2, 2), true); 168 | assert_eq!(td.get_pixel(4, 0), true); 169 | assert_eq!(td.get_pixel(4, 2), false); 170 | let td = td.rotated(); 171 | // 1 1 0 0 172 | // 1 1 0 0 173 | // 0 0 1 1 174 | // 0 0 1 1 175 | // 1 1 0 0 176 | // 1 1 0 0 177 | println!("{:?}", td.framebuffer); 178 | assert_eq!(td.get_pixel(0, 0), true); 179 | assert_eq!(td.get_pixel(0, 2), false); 180 | assert_eq!(td.get_pixel(2, 0), false); 181 | assert_eq!(td.get_pixel(2, 2), true); 182 | assert_eq!(td.get_pixel(0, 4), true); 183 | assert_eq!(td.get_pixel(2, 4), false); 184 | } 185 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(new_uninit)] 2 | #![feature(slice_take)] 3 | #![feature(exclusive_range_pattern)] 4 | pub mod analyzer; 5 | pub mod display; 6 | pub mod print; 7 | pub mod protocol; 8 | 9 | use crate::print::mm_to_px; 10 | use crate::protocol::PacketHeader; 11 | use anyhow::anyhow; 12 | use anyhow::Result; 13 | 14 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 15 | pub enum Tape { 16 | W4, 17 | W6, 18 | W9, 19 | W12, 20 | W18, 21 | W24, 22 | W36, 23 | } 24 | impl Tape { 25 | pub fn from_mm(mm: usize) -> Result { 26 | Ok(match mm { 27 | 4 => Tape::W4, 28 | 6 => Tape::W6, 29 | 9 => Tape::W9, 30 | 12 => Tape::W12, 31 | 18 => Tape::W18, 32 | 24 => Tape::W24, 33 | 36 => Tape::W36, 34 | _ => return Err(anyhow!("Tape for {mm} mm is not defined")), 35 | }) 36 | } 37 | fn width_px(&self) -> i32 { 38 | let w = match self { 39 | Tape::W4 => 2.85, // verified 40 | Tape::W6 => 5.0, // verified 41 | Tape::W9 => 7.0, // verified 42 | Tape::W12 => 10.0, // verified 43 | Tape::W18 => 15.2, // verified 44 | Tape::W24 => 20.0, // verified 45 | Tape::W36 => 26.0, // verified 46 | }; 47 | let w = mm_to_px(w); 48 | // tape width in px should be multiple of 8 49 | (w + 7) / 8 * 8 50 | } 51 | } 52 | 53 | #[derive(Copy, Clone, Debug)] 54 | pub enum PrinterStatus { 55 | NoTape, 56 | SomeTape(Tape), 57 | CoverIsOpened, 58 | Printing, 59 | Unknown(PacketHeader, [u8; 20]), 60 | } 61 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(new_uninit)] 2 | #![feature(slice_take)] 3 | #![feature(exclusive_range_pattern)] 4 | 5 | use anyhow::Result; 6 | use argh::FromArgs; 7 | use sr5900p::analyzer::analyze_tcp_data; 8 | use sr5900p::print::do_print; 9 | use sr5900p::print::PrintArgs; 10 | use std::fs; 11 | 12 | #[derive(FromArgs, PartialEq, Debug)] 13 | /// Analyze the packet captures 14 | #[argh(subcommand, name = "analyze")] 15 | struct AnalyzeArgs { 16 | /// the raw dump of the TCP stream while printing 17 | #[argh(option)] 18 | tcp_data: String, 19 | } 20 | fn do_analyze(dump_file: &str) -> Result<()> { 21 | let data = fs::read(dump_file)?; 22 | analyze_tcp_data(&data) 23 | } 24 | 25 | #[derive(FromArgs, PartialEq, Debug)] 26 | #[argh(subcommand)] 27 | enum ArgsSubCommand { 28 | Analyze(AnalyzeArgs), 29 | Print(PrintArgs), 30 | } 31 | #[derive(Debug, FromArgs)] 32 | /// Reach new heights. 33 | struct Args { 34 | #[argh(subcommand)] 35 | nested: ArgsSubCommand, 36 | } 37 | 38 | fn main() -> Result<()> { 39 | let args: Args = argh::from_env(); 40 | println!("{:?}", args); 41 | match args.nested { 42 | ArgsSubCommand::Analyze(args) => do_analyze(&args.tcp_data), 43 | ArgsSubCommand::Print(args) => do_print(&args), 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/print.rs: -------------------------------------------------------------------------------- 1 | use crate::analyzer::analyze_tcp_data; 2 | use crate::display::TapeDisplay; 3 | use crate::protocol::notify_data_stream; 4 | use crate::protocol::StartPrintRequest; 5 | use crate::protocol::StatusRequest; 6 | use crate::protocol::StopPrintRequest; 7 | use crate::PrinterStatus; 8 | use crate::Tape; 9 | use anyhow::anyhow; 10 | use anyhow::Context; 11 | use anyhow::Result; 12 | use argh::FromArgs; 13 | //use barcoders::sym::code39::Code39; 14 | use embedded_graphics::geometry::Dimensions; 15 | use embedded_graphics::geometry::Point; 16 | use embedded_graphics::mono_font::ascii::FONT_10X20; 17 | use embedded_graphics::mono_font::MonoTextStyle; 18 | use embedded_graphics::pixelcolor::BinaryColor; 19 | use embedded_graphics::prelude::Size; 20 | use embedded_graphics::primitives::PrimitiveStyle; 21 | use embedded_graphics::primitives::Rectangle; 22 | use embedded_graphics::primitives::StyledDrawable; 23 | use embedded_graphics::text::Alignment; 24 | use embedded_graphics::text::Baseline; 25 | use embedded_graphics::text::Text; 26 | use embedded_graphics::text::TextStyleBuilder; 27 | use embedded_graphics::Drawable; 28 | use image::Luma; 29 | use qrcode::QrCode; 30 | //use regex::Regex; 31 | //use std::fs; 32 | use std::fs::File; 33 | use std::io::prelude::Write; 34 | use std::io::BufWriter; 35 | use std::net::TcpStream; 36 | use std::net::UdpSocket; 37 | use std::num::Wrapping; 38 | use std::path::Path; 39 | use std::thread; 40 | use std::time; 41 | 42 | pub fn mm_to_px(mm: f32) -> i32 { 43 | const DPI: f32 = 360.0; 44 | const MM_TO_INCH: f32 = 10.0 / 254.0; 45 | (mm * DPI * MM_TO_INCH).floor() as i32 46 | } 47 | 48 | fn print_tcp_data(device_ip: &str, data: &[u8]) -> Result<()> { 49 | let socket = UdpSocket::bind("0.0.0.0:0").context("failed to bind")?; 50 | let info = StatusRequest::send(&socket, device_ip)?; 51 | println!("{:?}", info); 52 | if let PrinterStatus::SomeTape(t) = info { 53 | println!("Tape is {:?}, start printing...", t); 54 | } else { 55 | println!("Unexpected state. Aborting..."); 56 | std::process::exit(1); 57 | } 58 | StartPrintRequest::send(&socket, device_ip)?; 59 | thread::sleep(time::Duration::from_millis(500)); 60 | let mut stream = TcpStream::connect(device_ip.to_string() + ":9100")?; 61 | thread::sleep(time::Duration::from_millis(500)); 62 | notify_data_stream(&socket, device_ip)?; 63 | thread::sleep(time::Duration::from_millis(500)); 64 | stream.write_all(data)?; 65 | 66 | println!("Print data is sent. Waiting..."); 67 | loop { 68 | thread::sleep(time::Duration::from_millis(500)); 69 | let info = StatusRequest::send(&socket, device_ip)?; 70 | println!("{:?}", info); 71 | if let PrinterStatus::Printing = info { 72 | continue; 73 | } 74 | break; 75 | } 76 | 77 | StopPrintRequest::send(&socket, device_ip)?; 78 | 79 | Ok(()) 80 | } 81 | 82 | fn gen_tcp_data(td: &TapeDisplay) -> Result> { 83 | let mut tcp_data: Vec = Vec::new(); 84 | tcp_data.append(&mut vec![27, 123, 3, 64, 64, 125]); 85 | tcp_data.append(&mut vec![27, 123, 7, 123, 0, 0, 83, 84, 34, 125]); 86 | tcp_data.append(&mut vec![27, 123, 7, 67, 2, 2, 1, 1, 73, 125]); // half-cut? 87 | tcp_data.append(&mut vec![27, 123, 4, 68, 5, 73, 125]); 88 | tcp_data.append(&mut vec![27, 123, 3, 71, 71, 125]); 89 | 90 | let mut tape_len_bytes = (td.width as u32 + 4/* safe margin */) 91 | .to_le_bytes() 92 | .to_vec(); 93 | let mut cmd_bytes = vec![76]; 94 | cmd_bytes.append(&mut tape_len_bytes); 95 | let csum = cmd_bytes 96 | .iter() 97 | .map(|v| Wrapping(*v)) 98 | .sum::>() 99 | .0; 100 | cmd_bytes.push(csum); 101 | cmd_bytes.push(0x7d); 102 | tcp_data.append(&mut vec![0x1b, 0x7b, cmd_bytes.len() as u8]); 103 | tcp_data.append(&mut cmd_bytes); 104 | 105 | tcp_data.append(&mut vec![27, 123, 5, 84, 42, 0, 126, 125]); 106 | tcp_data.append(&mut vec![27, 123, 4, 72, 5, 77, 125]); 107 | tcp_data.append(&mut vec![27, 123, 4, 115, 0, 115, 125]); 108 | 109 | let row_bytes = (td.height + 7) / 8; 110 | for y in 0..td.width { 111 | tcp_data.append(&mut vec![0x1b, 0x2e, 0, 0, 0, 1]); 112 | tcp_data.append(&mut (td.height as u16).to_le_bytes().to_vec()); 113 | for xb in 0..row_bytes { 114 | let mut chunk = 0x00; 115 | for dx in 0..8 { 116 | let x = xb * 8 + (7 - dx); 117 | 118 | if td.get_pixel(td.width - 1 - y, x) { 119 | chunk |= 1 << dx 120 | } 121 | } 122 | tcp_data.push(chunk); 123 | } 124 | } 125 | tcp_data.push(0x0c); // data end 126 | tcp_data.append(&mut vec![27, 123, 3, 64, 64, 125]); 127 | Ok(tcp_data) 128 | } 129 | 130 | /* 131 | fn print_mac_addr(mac_addr: &str, device_ip: &str, dry_run: bool) -> Result<()> { 132 | let text = mac_addr.to_uppercase().replace(":", ""); 133 | println!("{:?}", text); 134 | let re = Regex::new(r"^[0-9A-Z]{12}$").unwrap(); 135 | if !re.is_match(&text) { 136 | return Err(anyhow!("Invalid MAC Address: {mac_addr}")); 137 | } 138 | let socket = UdpSocket::bind("0.0.0.0:0").context("failed to bind")?; 139 | let info = StatusRequest::send(&socket, device_ip)?; 140 | let tape_width_px = if let PrinterStatus::SomeTape(t) = info { 141 | println!("Tape is {:?}", t); 142 | match t { 143 | // -4mm will be the printable width... 144 | Tape::W9 => 5 * 360 * 10 / 254, 145 | Tape::W12 => 10 * 360 * 10 / 254, 146 | Tape::W18 => 14 * 360 * 10 / 254, // verified 147 | Tape::W24 => 20 * 360 * 10 / 254, 148 | Tape::W36 => 26 * 360 * 10 / 254, // verified 149 | _ => return Err(anyhow!("Failed to calc tape width. status: {:?}", info)), 150 | } 151 | } else { 152 | return Err(anyhow!( 153 | "Failed to determine tape width. status: {:?}", 154 | info 155 | )); 156 | }; 157 | let tape_width_px = (tape_width_px + 7) / 8 * 8; 158 | 159 | let qr_td = { 160 | let mut td = TapeDisplay::new(tape_width_px, tape_width_px); 161 | let tape_width_px = tape_width_px as u32; 162 | let code = QrCode::new(&text).unwrap(); 163 | let image = code 164 | .render::>() 165 | .max_dimensions(tape_width_px, tape_width_px) 166 | .build(); 167 | let ofs_x = (tape_width_px - image.width()) / 2; 168 | let ofs_y = (tape_width_px - image.height()) / 2; 169 | for (x, y, p) in image.enumerate_pixels() { 170 | Rectangle::new( 171 | Point::new((x + ofs_x) as i32, (y + ofs_y) as i32), 172 | Size::new_equal(1), 173 | ) 174 | .draw_styled( 175 | &PrimitiveStyle::with_fill(BinaryColor::from(p.0[0] == 0)), 176 | &mut td, 177 | )?; 178 | } 179 | image.save("qrcode.png").unwrap(); 180 | td 181 | }; 182 | 183 | let barcode = Code39::new(&text).context("Failed to generate a barcode")?; 184 | let encoded: Vec = barcode.encode(); 185 | println!("{:?}", encoded); 186 | 187 | let mac_td = { 188 | let character_style = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); 189 | let text_len = text.len(); 190 | let mut td = TapeDisplay::new(10 * text_len, 20); 191 | Text::with_alignment( 192 | &text, 193 | td.bounding_box().center() + Point::new(0, 10), 194 | character_style, 195 | Alignment::Center, 196 | ) 197 | .draw(&mut td)?; 198 | let td = td.rotated(); 199 | let r = qr_td.height / td.height; 200 | td.scaled(r) 201 | }; 202 | 203 | // Merge the components 204 | let mut td = TapeDisplay::new(qr_td.width + mac_td.width, tape_width_px); 205 | td.overlay_or(&mac_td, 0, 0); 206 | td.overlay_or(&qr_td, mac_td.width, 0); 207 | 208 | // Generate preview image 209 | let path = Path::new(r"preview.png"); 210 | let file = File::create(path).unwrap(); 211 | let ref mut w = BufWriter::new(file); 212 | let mut encoder = png::Encoder::new(w, td.width as u32, td.height as u32); // Width is 2 pixels and height is 1. 213 | encoder.set_color(png::ColorType::Rgba); 214 | encoder.set_depth(png::BitDepth::Eight); 215 | encoder.set_source_gamma(png::ScaledFloat::from_scaled(45455)); // 1.0 / 2.2, scaled by 100000 216 | encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2)); // 1.0 / 2.2, unscaled, but rounded 217 | let source_chromaticities = png::SourceChromaticities::new( 218 | // Using unscaled instantiation here 219 | (0.31270, 0.32900), 220 | (0.64000, 0.33000), 221 | (0.30000, 0.60000), 222 | (0.15000, 0.06000), 223 | ); 224 | encoder.set_source_chromaticities(source_chromaticities); 225 | let mut writer = encoder.write_header().unwrap(); 226 | let data: Vec = td 227 | .framebuffer 228 | .iter() 229 | .flat_map(|row| row.iter()) 230 | .flat_map(|c| { 231 | // data will be [RGBARGBA...] 232 | if *c { 233 | [0, 0, 0, 255] 234 | } else { 235 | [255, 255, 255, 255] 236 | } 237 | }) 238 | .collect(); 239 | writer.write_image_data(&data).unwrap(); 240 | 241 | let tcp_data = gen_tcp_data(&td)?; 242 | 243 | if !dry_run { 244 | print_tcp_data(device_ip, &tcp_data) 245 | } else { 246 | analyze_tcp_data(&tcp_data)?; 247 | Ok(()) 248 | } 249 | } 250 | 251 | fn print_qr_text(text: &str, device_ip: &str, dry_run: bool) -> Result<()> { 252 | println!("{:?}", text); 253 | let socket = UdpSocket::bind("0.0.0.0:0").context("failed to bind")?; 254 | let info = StatusRequest::send(&socket, device_ip)?; 255 | let tape_width_px = if let PrinterStatus::SomeTape(t) = info { 256 | println!("Tape is {:?}", t); 257 | match t { 258 | // -4mm will be the printable width... 259 | Tape::W9 => 5 * 360 * 10 / 254, 260 | Tape::W12 => 10 * 360 * 10 / 254, 261 | Tape::W18 => 14 * 360 * 10 / 254, // verified 262 | Tape::W24 => 20 * 360 * 10 / 254, 263 | Tape::W36 => 26 * 360 * 10 / 254, // verified 264 | _ => return Err(anyhow!("Failed to calc tape width. status: {:?}", info)), 265 | } 266 | } else { 267 | return Err(anyhow!( 268 | "Failed to determine tape width. status: {:?}", 269 | info 270 | )); 271 | }; 272 | let tape_width_px = (tape_width_px + 7) / 8 * 8; 273 | 274 | let qr_td = { 275 | let mut td = TapeDisplay::new(tape_width_px, tape_width_px); 276 | let tape_width_px = tape_width_px as u32; 277 | let code = QrCode::new(&text).unwrap(); 278 | let image = code 279 | .render::>() 280 | .max_dimensions(tape_width_px, tape_width_px) 281 | .build(); 282 | let ofs_x = (tape_width_px - image.width()) / 2; 283 | let ofs_y = (tape_width_px - image.height()) / 2; 284 | for (x, y, p) in image.enumerate_pixels() { 285 | Rectangle::new( 286 | Point::new((x + ofs_x) as i32, (y + ofs_y) as i32), 287 | Size::new_equal(1), 288 | ) 289 | .draw_styled( 290 | &PrimitiveStyle::with_fill(BinaryColor::from(p.0[0] == 0)), 291 | &mut td, 292 | )?; 293 | } 294 | image.save("qrcode.png").unwrap(); 295 | td 296 | }; 297 | 298 | let text_td = { 299 | let character_style = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); 300 | let text_len = text.len(); 301 | let mut td = TapeDisplay::new(10 * text_len, 20); 302 | Text::with_alignment( 303 | &text, 304 | td.bounding_box().center() + Point::new(0, 10), 305 | character_style, 306 | Alignment::Center, 307 | ) 308 | .draw(&mut td)?; 309 | let r = tape_width_px / td.height; 310 | td.scaled(r) 311 | }; 312 | 313 | // Merge the components 314 | let mut td = TapeDisplay::new(qr_td.width + text_td.width, tape_width_px); 315 | td.overlay_or(&qr_td, 0, 0); 316 | td.overlay_or(&text_td, qr_td.width, (tape_width_px - text_td.height) / 2); 317 | 318 | // Generate preview image 319 | let path = Path::new(r"preview.png"); 320 | let file = File::create(path).unwrap(); 321 | let ref mut w = BufWriter::new(file); 322 | let mut encoder = png::Encoder::new(w, td.width as u32, td.height as u32); // Width is 2 pixels and height is 1. 323 | encoder.set_color(png::ColorType::Rgba); 324 | encoder.set_depth(png::BitDepth::Eight); 325 | encoder.set_source_gamma(png::ScaledFloat::from_scaled(45455)); // 1.0 / 2.2, scaled by 100000 326 | encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2)); // 1.0 / 2.2, unscaled, but rounded 327 | let source_chromaticities = png::SourceChromaticities::new( 328 | // Using unscaled instantiation here 329 | (0.31270, 0.32900), 330 | (0.64000, 0.33000), 331 | (0.30000, 0.60000), 332 | (0.15000, 0.06000), 333 | ); 334 | encoder.set_source_chromaticities(source_chromaticities); 335 | let mut writer = encoder.write_header().unwrap(); 336 | let data: Vec = td 337 | .framebuffer 338 | .iter() 339 | .flat_map(|row| row.iter()) 340 | .flat_map(|c| { 341 | // data will be [RGBARGBA...] 342 | if *c { 343 | [0, 0, 0, 255] 344 | } else { 345 | [255, 255, 255, 255] 346 | } 347 | }) 348 | .collect(); 349 | writer.write_image_data(&data).unwrap(); 350 | 351 | let tcp_data = gen_tcp_data(&td)?; 352 | 353 | if !dry_run { 354 | print_tcp_data(device_ip, &tcp_data) 355 | } else { 356 | analyze_tcp_data(&tcp_data)?; 357 | Ok(()) 358 | } 359 | } 360 | */ 361 | 362 | fn determine_tape_width_px(args: &PrintArgs) -> Result { 363 | let detected = if let Some(printer) = &args.printer { 364 | let socket = UdpSocket::bind("0.0.0.0:0").context("failed to bind")?; 365 | let info = StatusRequest::send(&socket, printer)?; 366 | eprintln!("Tape detected: {:?}", info); 367 | if let PrinterStatus::SomeTape(t) = info { 368 | Some(t) 369 | } else { 370 | eprintln!("Failed to detect tape width. status: {:?}", info); 371 | None 372 | } 373 | } else { 374 | None 375 | }; 376 | let given = if let Some(mm) = args.width { 377 | Some(Tape::from_mm(mm)?) 378 | } else { 379 | None 380 | }; 381 | Ok(match (given, detected) { 382 | (None, Some(w)) | (Some(w), None) => w, 383 | (Some(given), Some(detected)) => { 384 | if given != detected { 385 | eprintln!("Warning: {given:?} does not match with detected {detected:?}") 386 | } 387 | given 388 | } 389 | (None, None) => return Err(anyhow!("Please specify --width or --printer")), 390 | } 391 | .width_px()) 392 | } 393 | 394 | fn print_qr_text(args: &PrintArgs) -> Result<()> { 395 | let text = args.qr_text.as_ref().expect("Please specify --qr-text"); 396 | let tape_width_px = determine_tape_width_px(args)? as usize; 397 | let qr_td = { 398 | let mut td = TapeDisplay::new(tape_width_px, tape_width_px); 399 | let tape_width_px = tape_width_px as u32; 400 | let code = QrCode::new(text).unwrap(); 401 | let image = code 402 | .render::>() 403 | .max_dimensions(tape_width_px, tape_width_px) 404 | .build(); 405 | let ofs_x = (tape_width_px - image.width()) / 2; 406 | let ofs_y = (tape_width_px - image.height()) / 2; 407 | for (x, y, p) in image.enumerate_pixels() { 408 | Rectangle::new( 409 | Point::new((x + ofs_x) as i32, (y + ofs_y) as i32), 410 | Size::new_equal(1), 411 | ) 412 | .draw_styled( 413 | &PrimitiveStyle::with_fill(BinaryColor::from(p.0[0] == 0)), 414 | &mut td, 415 | )?; 416 | } 417 | image.save("qrcode.png").unwrap(); 418 | td 419 | }; 420 | let text_td = { 421 | let character_style = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); 422 | let text_len = text.len(); 423 | let margin_px = 4usize; 424 | let r = tape_width_px / (20 + margin_px); 425 | let mut td = TapeDisplay::new(10 * text_len + margin_px, 20 + margin_px); 426 | let tb = TextStyleBuilder::new(); 427 | let ts = tb 428 | .alignment(Alignment::Center) 429 | .baseline(Baseline::Middle) 430 | .build(); 431 | Text::with_text_style(text, td.bounding_box().center(), character_style, ts) 432 | .draw(&mut td)?; 433 | // magnify the td as much as possible to fit the parent 434 | td.scaled(r) 435 | }; 436 | let mut td = TapeDisplay::new(qr_td.width + text_td.width, tape_width_px); 437 | td.overlay_or(&qr_td, 0, (td.height - qr_td.height) / 2); 438 | td.overlay_or(&text_td, qr_td.width, (td.height - text_td.height) / 2); 439 | print_td(args, &td) 440 | } 441 | 442 | fn print_qr_text_small(args: &PrintArgs) -> Result<()> { 443 | let text = args.qr_text_small.as_ref().expect("Please specify --qr-text-small"); 444 | let tape_width_px = determine_tape_width_px(args)? as usize; 445 | let qr_td = { 446 | let mut td = TapeDisplay::new(tape_width_px, tape_width_px); 447 | let tape_width_px = tape_width_px as u32; 448 | let code = QrCode::new(text).unwrap(); 449 | let image = code 450 | .render::>() 451 | .max_dimensions(tape_width_px, tape_width_px) 452 | .build(); 453 | let ofs_x = (tape_width_px - image.width()) / 2; 454 | let ofs_y = (tape_width_px - image.height()) / 2; 455 | for (x, y, p) in image.enumerate_pixels() { 456 | Rectangle::new( 457 | Point::new((x + ofs_x) as i32, (y + ofs_y) as i32), 458 | Size::new_equal(1), 459 | ) 460 | .draw_styled( 461 | &PrimitiveStyle::with_fill(BinaryColor::from(p.0[0] == 0)), 462 | &mut td, 463 | )?; 464 | } 465 | image.save("qrcode.png").unwrap(); 466 | td 467 | }; 468 | let text_td = { 469 | let character_style = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); 470 | let text_len = text.len(); 471 | let margin_px = 4usize; 472 | let text_width = 10 * text_len + margin_px; 473 | let r = std::cmp::min(tape_width_px / text_width, 8); 474 | let mut td = TapeDisplay::new(text_width, 20+margin_px); 475 | let tb = TextStyleBuilder::new(); 476 | let ts = tb 477 | .alignment(Alignment::Center) 478 | .baseline(Baseline::Middle) 479 | .build(); 480 | Text::with_text_style(text, td.bounding_box().center(), character_style, ts) 481 | .draw(&mut td)?; 482 | // magnify the td as much as possible to fit the parent 483 | td.rotated().scaled(r) 484 | }; 485 | let mut td = TapeDisplay::new(qr_td.width * 9 / 10 + text_td.width, tape_width_px); 486 | td.overlay_or(&qr_td, 0, (td.height - qr_td.height) / 2); 487 | td.overlay_or(&text_td, qr_td.width * 9 / 10, (td.height - text_td.height)/2); 488 | print_td(args, &td) 489 | } 490 | 491 | fn print_td(args: &PrintArgs, td: &TapeDisplay) -> Result<()> { 492 | // Generate preview image 493 | let path = Path::new(r"preview.png"); 494 | let file = File::create(path).unwrap(); 495 | let w = BufWriter::new(file); 496 | let mut encoder = png::Encoder::new(w, td.width as u32, td.height as u32); // Width is 2 pixels and height is 1. 497 | encoder.set_color(png::ColorType::Rgba); 498 | encoder.set_depth(png::BitDepth::Eight); 499 | encoder.set_source_gamma(png::ScaledFloat::from_scaled(45455)); // 1.0 / 2.2, scaled by 100000 500 | encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2)); // 1.0 / 2.2, unscaled, but rounded 501 | let source_chromaticities = png::SourceChromaticities::new( 502 | // Using unscaled instantiation here 503 | (0.31270, 0.32900), 504 | (0.64000, 0.33000), 505 | (0.30000, 0.60000), 506 | (0.15000, 0.06000), 507 | ); 508 | encoder.set_source_chromaticities(source_chromaticities); 509 | let mut writer = encoder.write_header().unwrap(); 510 | let data: Vec = td 511 | .framebuffer 512 | .iter() 513 | .flat_map(|row| row.iter()) 514 | .flat_map(|c| { 515 | // data will be [RGBARGBA...] 516 | if *c { 517 | [0, 0, 0, 255] 518 | } else { 519 | [255, 255, 255, 255] 520 | } 521 | }) 522 | .collect(); 523 | writer.write_image_data(&data).unwrap(); 524 | 525 | let tcp_data = gen_tcp_data(td)?; 526 | 527 | if !args.dry_run { 528 | print_tcp_data( 529 | args.printer.as_ref().context("Please specify --printer")?, 530 | &tcp_data, 531 | ) 532 | } else { 533 | analyze_tcp_data(&tcp_data)?; 534 | Ok(()) 535 | } 536 | } 537 | 538 | fn print_test_pattern(args: &PrintArgs) -> Result<()> { 539 | let tape_width_px = determine_tape_width_px(args)?; 540 | // td represents a tape segment 541 | let mut td = TapeDisplay::new(mm_to_px(40.0) as usize, tape_width_px as usize); 542 | // 1mm outline 543 | Rectangle::new( 544 | Point::new(0, 0), 545 | Size { 546 | width: td.width as u32, 547 | height: td.height as u32, 548 | }, 549 | ) 550 | .draw_styled(&PrimitiveStyle::with_fill(BinaryColor::from(true)), &mut td)?; 551 | Rectangle::new( 552 | Point::new(mm_to_px(1.0), mm_to_px(1.0)), 553 | Size { 554 | width: td.width as u32 - mm_to_px(2.0) as u32, 555 | height: td.height as u32 - mm_to_px(2.0) as u32, 556 | }, 557 | ) 558 | .draw_styled( 559 | &PrimitiveStyle::with_fill(BinaryColor::from(false)), 560 | &mut td, 561 | )?; 562 | // 0.5mm squares, at 1mm cells, from the print origin 563 | for y_mm in 0..40 { 564 | for x_mm in 0..40 { 565 | Rectangle::new( 566 | Point::new(mm_to_px(x_mm as f32), mm_to_px(y_mm as f32)), 567 | Size::new_equal(mm_to_px(0.5) as u32), 568 | ) 569 | .draw_styled(&PrimitiveStyle::with_fill(BinaryColor::from(true)), &mut td)?; 570 | } 571 | } 572 | // 1cm square 573 | Rectangle::new( 574 | Point::new(0, 0), 575 | Size { 576 | width: mm_to_px(10.0) as u32, 577 | height: mm_to_px(10.0) as u32, 578 | }, 579 | ) 580 | .draw_styled(&PrimitiveStyle::with_fill(BinaryColor::from(true)), &mut td)?; 581 | // 0.5cm square, at the center of the previous one 582 | Rectangle::new( 583 | Point::new(mm_to_px(2.5), mm_to_px(2.5)), 584 | Size { 585 | width: mm_to_px(5.0) as u32, 586 | height: mm_to_px(5.0) as u32, 587 | }, 588 | ) 589 | .draw_styled( 590 | &PrimitiveStyle::with_fill(BinaryColor::from(false)), 591 | &mut td, 592 | )?; 593 | let text_td = { 594 | let text = "Ag"; 595 | let character_style = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); 596 | let text_len = text.len(); 597 | let margin_px = 4; 598 | let r = (td.height / (20 + margin_px)) as i32; 599 | let mut td = TapeDisplay::new(10 * text_len + margin_px, 20 + margin_px); 600 | // 1px outline (in text td) 601 | Rectangle::new( 602 | Point::new(0, 0), 603 | Size { 604 | width: td.width as u32, 605 | height: td.height as u32, 606 | }, 607 | ) 608 | .draw_styled(&PrimitiveStyle::with_fill(BinaryColor::from(true)), &mut td)?; 609 | Rectangle::new( 610 | Point::new(1, 1), 611 | Size { 612 | width: td.width as u32 - 2, 613 | height: td.height as u32 - 2, 614 | }, 615 | ) 616 | .draw_styled( 617 | &PrimitiveStyle::with_fill(BinaryColor::from(false)), 618 | &mut td, 619 | )?; 620 | let tb = TextStyleBuilder::new(); 621 | let ts = tb 622 | .alignment(Alignment::Center) 623 | .baseline(Baseline::Middle) 624 | .build(); 625 | Text::with_text_style(text, td.bounding_box().center(), character_style, ts) 626 | .draw(&mut td)?; 627 | // magnify the td as much as possible to fit the parent 628 | td.scaled(r as usize) 629 | }; 630 | td.overlay_or( 631 | &text_td, 632 | (td.width - text_td.width) / 2, 633 | (td.height - text_td.height) / 2, 634 | ); 635 | print_td(args, &td) 636 | } 637 | 638 | #[derive(FromArgs, PartialEq, Debug)] 639 | /// Print something 640 | #[argh(subcommand, name = "print")] 641 | pub struct PrintArgs { 642 | /// generate a label for a mac addr 643 | #[argh(option)] 644 | mac_addr: Option, 645 | /// generate a label for a QR code with text 646 | #[argh(option)] 647 | qr_text: Option, 648 | /// generate a label for a QR code with text 649 | #[argh(option)] 650 | qr_text_small: Option, 651 | /// tape width in mm (default: auto) 652 | #[argh(option)] 653 | width: Option, 654 | /// do not print (just generate and analyze) 655 | #[argh(switch)] 656 | dry_run: bool, 657 | /// the raw dump of the TCP stream while printing 658 | #[argh(option)] 659 | tcp_data: Option, 660 | /// print a test pattern 661 | #[argh(switch)] 662 | test_pattern: bool, 663 | /// an IPv4 address for the printer 664 | #[argh(option)] 665 | printer: Option, 666 | } 667 | pub fn do_print(args: &PrintArgs) -> Result<()> { 668 | if args.test_pattern { 669 | print_test_pattern(args) 670 | } else if args.qr_text.is_some() { 671 | print_qr_text(args) 672 | } else if args.qr_text_small.is_some() { 673 | print_qr_text_small(args) 674 | } else { 675 | Err(anyhow!("Please specify a print command")) 676 | } 677 | } 678 | -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | use crate::PrinterStatus; 2 | use crate::Tape; 3 | use anyhow::anyhow; 4 | use anyhow::Context; 5 | use anyhow::Result; 6 | use std::boxed::Box; 7 | use std::mem::size_of; 8 | use std::mem::MaybeUninit; 9 | use std::net::UdpSocket; 10 | use std::slice; 11 | 12 | /// # Safety 13 | /// Implementing this trait is safe only when the target type can be converted 14 | /// mutually between a byte sequence of the same size, which means that no ownership 15 | /// nor memory references are involved. 16 | pub unsafe trait Sliceable: Sized + Copy + Clone { 17 | fn copy_into_slice(&self) -> Box<[u8]> { 18 | let mut values = Box::<[u8]>::new_uninit_slice(size_of::()); 19 | unsafe { 20 | values.copy_from_slice(slice::from_raw_parts( 21 | self as *const Self as *const MaybeUninit, 22 | size_of::(), 23 | )); 24 | values.assume_init() 25 | } 26 | } 27 | fn copy_from_slice(data: &[u8]) -> Result { 28 | if size_of::() > data.len() { 29 | Err(anyhow!("data is too short")) 30 | } else { 31 | Ok(unsafe { *(data.as_ptr() as *const Self) }) 32 | } 33 | } 34 | } 35 | unsafe impl Sliceable for PacketHeader {} 36 | unsafe impl Sliceable for StatusRequest {} 37 | unsafe impl Sliceable for StartPrintRequest {} 38 | unsafe impl Sliceable for StopPrintRequest {} 39 | 40 | #[repr(packed)] 41 | #[derive(Copy, Clone, Debug)] 42 | pub struct PacketHeader { 43 | _signature: [u8; 4], // "TPRT" for requests, "tprt" for responses 44 | _const00_be: [u8; 4], // 00 00 00 00 45 | _const01_be: [u8; 4], // 00 00 00 01 46 | _const20_be: [u8; 4], // 00 00 00 20 47 | _cmd_be: [u8; 4], 48 | _data_size_be: [u8; 4], 49 | _ip_addr_be: [u8; 4], 50 | _token_be: [u8; 4], 51 | } 52 | impl PacketHeader { 53 | pub fn new_request(cmd: u32, data_size: u32) -> Self { 54 | Self { 55 | _signature: *b"TPRT", 56 | _const00_be: 0x00u32.to_be_bytes(), 57 | _const01_be: 0x01u32.to_be_bytes(), 58 | _const20_be: 0x20u32.to_be_bytes(), 59 | _cmd_be: cmd.to_be_bytes(), 60 | _data_size_be: data_size.to_be_bytes(), 61 | _ip_addr_be: 0x00u32.to_be_bytes(), 62 | _token_be: 0x00u32.to_be_bytes(), 63 | } 64 | } 65 | } 66 | 67 | #[repr(packed)] 68 | #[derive(Copy, Clone)] 69 | pub struct StatusRequest { 70 | _header: PacketHeader, 71 | } 72 | impl StatusRequest { 73 | fn new() -> Self { 74 | Self { 75 | _header: PacketHeader::new_request(1, 0), 76 | } 77 | } 78 | pub fn send(socket: &UdpSocket, device_ip: &str) -> Result { 79 | let req = Self::new(); 80 | socket 81 | .send_to(&req.copy_into_slice(), device_ip.to_string() + ":9100") 82 | .context("failed to send")?; 83 | let mut buf = [0; 128]; 84 | let (len, _) = socket.recv_from(&mut buf)?; 85 | let res_header = PacketHeader::copy_from_slice(&buf[0..len])?; 86 | let data = &buf[size_of::()..len]; 87 | println!("{:?}", data); 88 | // idle 89 | // [20, 0, 0, 4, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 90 | // printing 91 | // [20, 2, 0, 4, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 92 | // printing completed 93 | // [20, 0, 0, 4, 0, 0, 0, 0, 64, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0] 94 | // Tape exhausted 95 | // [20, 0, 66, 4, 0, 0, 0, 0, 64, 0, 0, 64, 0, 0, 66, 0, 64, 0, 0, 0] 96 | // ??? 97 | // [20, 0, 0, 4, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 66, 0, 64, 0, 0, 0] 98 | let data: [u8; 20] = data.try_into().context(anyhow!( 99 | "invalid data len. expected 20 but got {}", 100 | data.len() 101 | ))?; 102 | Ok(match (data[0x01], data[0x0d]) { 103 | (2, 0) => PrinterStatus::Printing, 104 | (0, 0 | 1 | 2) => match data[0x02] { 105 | 0x06 => PrinterStatus::NoTape, 106 | 0x21 => PrinterStatus::CoverIsOpened, 107 | 0x00 => PrinterStatus::SomeTape(match data[0x03] { 108 | 0x01 => Tape::W6, 109 | 0x02 => Tape::W9, 110 | 0x03 => Tape::W12, 111 | 0x04 => Tape::W18, 112 | 0x05 => Tape::W24, 113 | 0x06 => Tape::W36, 114 | 0x0B => Tape::W4, 115 | ti => return Err(anyhow!("Unknow tape index {ti:#04X}")), 116 | }), 117 | _ => PrinterStatus::Unknown(res_header, data), 118 | }, 119 | (v01, v0d) => { 120 | eprintln!("v01: {v01}, v0d: {v0d}"); 121 | PrinterStatus::Unknown(res_header, data) 122 | } 123 | }) 124 | } 125 | } 126 | 127 | #[repr(packed)] 128 | #[derive(Copy, Clone)] 129 | pub struct StartPrintRequest { 130 | _header: PacketHeader, 131 | } 132 | impl StartPrintRequest { 133 | fn new() -> Self { 134 | Self { 135 | _header: PacketHeader::new_request(2, 0), 136 | } 137 | } 138 | pub fn send(socket: &UdpSocket, device_ip: &str) -> Result<()> { 139 | let req = Self::new(); 140 | socket 141 | .send_to(&req.copy_into_slice(), device_ip.to_string() + ":9100") 142 | .context("failed to send")?; 143 | let mut buf = [0; 128]; 144 | let (len, _) = socket.recv_from(&mut buf)?; 145 | let res_header = PacketHeader::copy_from_slice(&buf[0..len])?; 146 | let data = &buf[size_of::()..len]; 147 | if data == [2, 0, 0] { 148 | Ok(()) 149 | } else { 150 | Err(anyhow!( 151 | "Failed to start printing. res_header: {:?}, data: {:?}", 152 | res_header, 153 | data 154 | )) 155 | } 156 | } 157 | } 158 | 159 | #[repr(packed)] 160 | #[derive(Copy, Clone)] 161 | pub struct StopPrintRequest { 162 | _header: PacketHeader, 163 | } 164 | impl StopPrintRequest { 165 | fn new() -> Self { 166 | Self { 167 | _header: PacketHeader::new_request(3, 0), 168 | } 169 | } 170 | pub fn send(socket: &UdpSocket, device_ip: &str) -> Result<()> { 171 | let req = Self::new(); 172 | socket 173 | .send_to(&req.copy_into_slice(), device_ip.to_string() + ":9100") 174 | .context("failed to send")?; 175 | let mut buf = [0; 128]; 176 | let (len, _) = socket.recv_from(&mut buf)?; 177 | let res_header = PacketHeader::copy_from_slice(&buf[0..len])?; 178 | let data = &buf[size_of::()..len]; 179 | if data == [3, 0, 0] { 180 | Ok(()) 181 | } else { 182 | Err(anyhow!( 183 | "Failed to stop printing. res_header: {:?}, data: {:?}", 184 | res_header, 185 | data 186 | )) 187 | } 188 | } 189 | } 190 | 191 | pub fn notify_data_stream(socket: &UdpSocket, device_ip: &str) -> Result<()> { 192 | let mut buf = [0; 128]; 193 | 194 | let req = PacketHeader::new_request(0x0101, 0); 195 | socket 196 | .send_to(&req.copy_into_slice(), device_ip.to_string() + ":9100") 197 | .context("failed to send")?; 198 | socket.recv_from(&mut buf)?; 199 | 200 | let req = PacketHeader::new_request(0x0100, 0); 201 | socket 202 | .send_to(&req.copy_into_slice(), device_ip.to_string() + ":9100") 203 | .context("failed to send")?; 204 | let (len, _) = socket.recv_from(&mut buf)?; 205 | let res_header = PacketHeader::copy_from_slice(&buf[0..len])?; 206 | let data = &buf[size_of::()..len]; 207 | if data == [0x00] { 208 | println!("Warning: response for cmd 0x0100 was 0x00 (normally 0x10)"); 209 | } else if data != [0x10] { 210 | return Err(anyhow!( 211 | "Invalid response for cmd 0100: {:?}, data: {:?}", 212 | res_header, 213 | data 214 | )); 215 | } 216 | Ok(()) 217 | } 218 | --------------------------------------------------------------------------------