Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2325645bb5 | ||
|
|
46cca11e00 | ||
|
|
90b2cfd984 | ||
|
|
2bbf2c5c6b | ||
|
|
4a075c3d1e | ||
|
|
9dd8cc0574 | ||
|
|
676ff7630e | ||
|
|
4a41d1f5d7 | ||
|
|
5db8843ce7 | ||
|
|
34a268f0bf | ||
|
|
e3a0d7973d | ||
|
|
c30913f823 | ||
|
|
c80d7643ca | ||
|
|
9d872eecc9 | ||
|
|
2ee3d71e78 | ||
|
|
80b73922ac | ||
|
|
de6f4b2a4d | ||
|
|
621b085381 |
Generated
+10
-360
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
@@ -78,12 +78,8 @@ dependencies = [
|
||||
name = "atem-connection-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"derive-getters",
|
||||
"derive-new",
|
||||
"embassy-net",
|
||||
"enumflags2",
|
||||
"itertools",
|
||||
"log",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -140,12 +136,6 @@ version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.5.0"
|
||||
@@ -195,7 +185,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -237,44 +227,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "defmt"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"defmt-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-macros"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e"
|
||||
dependencies = [
|
||||
"defmt-parser",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-parser"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive-getters"
|
||||
version = "0.2.0"
|
||||
@@ -294,163 +246,7 @@ checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "embassy-net"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940c4b9fe5c1375b09a0c6722c0100d6b2ed46a717a34f632f26e8d7327c4383"
|
||||
dependencies = [
|
||||
"document-features",
|
||||
"embassy-net-driver",
|
||||
"embassy-sync",
|
||||
"embassy-time",
|
||||
"embedded-io-async",
|
||||
"embedded-nal-async",
|
||||
"heapless",
|
||||
"managed",
|
||||
"smoltcp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embassy-net-driver"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524eb3c489760508f71360112bca70f6e53173e6fe48fc5f0efd0f5ab217751d"
|
||||
|
||||
[[package]]
|
||||
name = "embassy-sync"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d2c8cdff05a7a51ba0087489ea44b0b1d97a296ca6b1d6d1a33ea7423d34049"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"critical-section",
|
||||
"embedded-io-async",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"heapless",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embassy-time"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f820157f198ada183ad62e0a66f554c610cdcd1a9f27d4b316358103ced7a1f8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"critical-section",
|
||||
"document-features",
|
||||
"embassy-time-driver",
|
||||
"embedded-hal 0.2.7",
|
||||
"embedded-hal 1.0.0",
|
||||
"embedded-hal-async",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embassy-time-driver"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d45f5d833b6d98bd2aab0c2de70b18bfaa10faf661a1578fd8e5dfb15eb7eba"
|
||||
dependencies = [
|
||||
"document-features",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-hal"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff"
|
||||
dependencies = [
|
||||
"nb 0.1.3",
|
||||
"void",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-hal"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-hal-async"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884"
|
||||
dependencies = [
|
||||
"embedded-hal 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io-async"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f"
|
||||
dependencies = [
|
||||
"embedded-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-nal"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c56a28be191a992f28f178ec338a0bf02f63d7803244add736d026a471e6ed77"
|
||||
dependencies = [
|
||||
"nb 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-nal-async"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76959917cd2b86f40a98c28dd5624eddd1fa69d746241c8257eac428d83cb211"
|
||||
dependencies = [
|
||||
"embedded-io-async",
|
||||
"embedded-nal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -478,9 +274,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
@@ -488,49 +284,12 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
|
||||
|
||||
[[package]]
|
||||
name = "hash32"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
||||
dependencies = [
|
||||
"hash32",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
@@ -564,15 +323,6 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
@@ -585,12 +335,6 @@ version = "0.2.153"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.6"
|
||||
@@ -609,12 +353,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "managed"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.0"
|
||||
@@ -642,21 +380,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nb"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f"
|
||||
dependencies = [
|
||||
"nb 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nb"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
@@ -717,39 +440,11 @@ version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -825,19 +520,6 @@ version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||
|
||||
[[package]]
|
||||
name = "smoltcp"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"cfg-if",
|
||||
"heapless",
|
||||
"managed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.6"
|
||||
@@ -848,12 +530,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
@@ -873,9 +549,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
version = "2.0.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -891,26 +567,6 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.3"
|
||||
@@ -947,7 +603,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1034,12 +690,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "void"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
[package]
|
||||
name = "atem-connection-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
derive-getters = "0.2.0"
|
||||
derive-new = "0.6.0"
|
||||
itertools = {version = "0.14.0", default-features = false}
|
||||
log = "0.4.14"
|
||||
tokio = { version = "1.13.0", features = ["full"], optional = true }
|
||||
tokio-util = { version = "0.7.10", optional = true }
|
||||
enumflags2 = { version = "0.7.12", default-features = false }
|
||||
embassy-net = { version = "0.7.0", optional = true, features = ["proto-ipv4", "medium-ip", "udp"]}
|
||||
defmt = {version = "1.0.0", optional = true}
|
||||
|
||||
[features]
|
||||
default = ["std", "embassy-net", "defmt"]
|
||||
|
||||
std = ["dep:tokio", "dep:tokio-util", "itertools/use_std"]
|
||||
embassy-net = ["dep:embassy-net"]
|
||||
defmt = ["dep:defmt"]
|
||||
tokio = { version = "1.13.0", features = ["full"] }
|
||||
tokio-util = "0.7.10"
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
use std::{
|
||||
boxed::Box,
|
||||
collections::{HashMap, VecDeque},
|
||||
net::SocketAddr,
|
||||
ops::DerefMut,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
use tokio::{select, sync::Semaphore};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
atem_lib::atem_socket::{
|
||||
AtemSocket, AtemSocketCommand, AtemSocketEvent, AtemSocketMessage, TrackingId,
|
||||
},
|
||||
atem_lib::atem_socket::{AtemEvent, AtemSocketCommand, AtemSocketMessage, TrackingId},
|
||||
commands::{
|
||||
command_base::{BasicWritableCommand, DeserializedCommand},
|
||||
device_profile::version::DESERIALIZE_VERSION_RAW_NAME,
|
||||
device_profile::DESERIALIZE_VERSION_RAW_NAME,
|
||||
init_complete::DESERIALIZE_INIT_COMPLETE_RAW_NAME,
|
||||
parse_commands::deserialize_commands,
|
||||
time::DESERIALIZE_TIME_RAW_NAME,
|
||||
},
|
||||
enums::ProtocolVersion,
|
||||
state::AtemState,
|
||||
};
|
||||
|
||||
@@ -35,24 +27,13 @@ pub enum AtemConnectionStatus {
|
||||
}
|
||||
|
||||
pub struct Atem {
|
||||
protocol_version: tokio::sync::RwLock<ProtocolVersion>,
|
||||
|
||||
socket: tokio::sync::RwLock<AtemSocket>,
|
||||
|
||||
waiting_semaphores: tokio::sync::RwLock<HashMap<TrackingId, Arc<Semaphore>>>,
|
||||
socket_message_tx: tokio::sync::mpsc::Sender<AtemSocketMessage>,
|
||||
}
|
||||
|
||||
impl Atem {
|
||||
pub fn new(
|
||||
socket: AtemSocket,
|
||||
socket_message_tx: tokio::sync::mpsc::Sender<AtemSocketMessage>,
|
||||
) -> Self {
|
||||
pub fn new(socket_message_tx: tokio::sync::mpsc::Sender<AtemSocketMessage>) -> Self {
|
||||
Self {
|
||||
protocol_version: tokio::sync::RwLock::new(ProtocolVersion::V7_2),
|
||||
|
||||
socket: tokio::sync::RwLock::new(socket),
|
||||
|
||||
waiting_semaphores: tokio::sync::RwLock::new(HashMap::new()),
|
||||
socket_message_tx,
|
||||
}
|
||||
@@ -73,30 +54,25 @@ impl Atem {
|
||||
|
||||
pub async fn run(
|
||||
&self,
|
||||
mut atem_event_rx: tokio::sync::mpsc::UnboundedReceiver<AtemSocketEvent>,
|
||||
mut atem_event_rx: tokio::sync::mpsc::UnboundedReceiver<AtemEvent>,
|
||||
cancel: CancellationToken,
|
||||
) {
|
||||
let mut status = AtemConnectionStatus::default();
|
||||
let mut state = AtemState::default();
|
||||
|
||||
let mut poll_interval = tokio::time::interval(Duration::from_millis(5));
|
||||
|
||||
while !cancel.is_cancelled() {
|
||||
let tick = poll_interval.tick();
|
||||
select! {
|
||||
_ = cancel.cancelled() => {},
|
||||
_ = tick => {},
|
||||
message = atem_event_rx.recv() => match message {
|
||||
Some(event) => match event {
|
||||
AtemSocketEvent::Connected => {
|
||||
AtemEvent::Connected => {
|
||||
log::info!("Atem connected");
|
||||
}
|
||||
AtemSocketEvent::Disconnected => todo!("Disconnected"),
|
||||
AtemSocketEvent::ReceivedCommands(payload) => {
|
||||
let commands = deserialize_commands(&payload, self.protocol_version.write().await.deref_mut());
|
||||
AtemEvent::Disconnected => todo!(),
|
||||
AtemEvent::ReceivedCommands(commands) => {
|
||||
self.mutate_state(&mut state, &mut status, commands).await
|
||||
}
|
||||
AtemSocketEvent::AckedCommand(tracking_id) => {
|
||||
AtemEvent::AckedCommand(tracking_id) => {
|
||||
log::debug!("Received tracking Id {tracking_id}");
|
||||
if let Some(semaphore) =
|
||||
self.waiting_semaphores.read().await.get(&tracking_id)
|
||||
@@ -113,19 +89,18 @@ impl Atem {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.socket.write().await.poll().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_commands(&self, commands: Vec<Box<dyn BasicWritableCommand>>) {
|
||||
let protocol_version = { self.protocol_version.read().await.clone() };
|
||||
let (callback_tx, callback_rx) = tokio::sync::oneshot::channel();
|
||||
self.socket_message_tx
|
||||
.send(AtemSocketMessage::SendCommands {
|
||||
commands: commands
|
||||
.iter()
|
||||
.map(|command| AtemSocketCommand::new(command, &protocol_version))
|
||||
.map(|command| {
|
||||
AtemSocketCommand::new(command, &crate::enums::ProtocolVersion::Unknown)
|
||||
})
|
||||
.collect(),
|
||||
tracking_ids_callback: callback_tx,
|
||||
})
|
||||
@@ -167,19 +142,14 @@ impl Atem {
|
||||
for command in commands {
|
||||
match command.raw_name() {
|
||||
DESERIALIZE_VERSION_RAW_NAME => {
|
||||
log::debug!("Received version response");
|
||||
*state = AtemState::default();
|
||||
*status = AtemConnectionStatus::Connecting
|
||||
}
|
||||
DESERIALIZE_INIT_COMPLETE_RAW_NAME => {
|
||||
log::debug!("Received init complete from ATEM");
|
||||
*status = AtemConnectionStatus::Connected
|
||||
}
|
||||
DESERIALIZE_INIT_COMPLETE_RAW_NAME => *status = AtemConnectionStatus::Connected,
|
||||
DESERIALIZE_TIME_RAW_NAME => {
|
||||
todo!("Time command")
|
||||
}
|
||||
_ => {
|
||||
log::debug!("Applying {} to state", command.raw_name());
|
||||
command.apply_to_state(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
//! Definitions and decoding of ATEM protocol fields
|
||||
|
||||
/// An uninterpreted ATEM protocol field - a 4-ascii character type plus variable length data
|
||||
pub struct RawField<'a> {
|
||||
pub r#type: &'a str,
|
||||
pub data: &'a [u8],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct _Ver {
|
||||
pub major: u16,
|
||||
pub minor: u16,
|
||||
}
|
||||
|
||||
impl<'a> Field<'a> for _Ver {
|
||||
const TYPE: &'static str = "_ver";
|
||||
|
||||
fn decode(data: &'a [u8]) -> Result<Self, FieldParsingError> {
|
||||
let data = checked_len::<4>(data)?;
|
||||
Ok(Self {
|
||||
major: u16::from_be_bytes(data[0..=1].try_into().unwrap()),
|
||||
minor: u16::from_be_bytes(data[2..=3].try_into().unwrap()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PrvI {
|
||||
pub m_e_index: u8,
|
||||
pub source_index: u16,
|
||||
pub pvw_in_pgm: bool,
|
||||
}
|
||||
|
||||
impl<'a> Field<'a> for PrvI {
|
||||
const TYPE: &'static str = "PrvI";
|
||||
fn decode(data: &'a [u8]) -> Result<Self, FieldParsingError> {
|
||||
let data = checked_len::<8>(data)?;
|
||||
Ok(Self {
|
||||
m_e_index: data[0],
|
||||
source_index: u16::from_be_bytes(data[2..=3].try_into().unwrap()),
|
||||
pvw_in_pgm: data[4] != 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PrgI {
|
||||
pub m_e_index: u8,
|
||||
pub source_index: u16,
|
||||
}
|
||||
|
||||
impl<'a> Field<'a> for PrgI {
|
||||
const TYPE: &'static str = "PrgI";
|
||||
fn decode(data: &'a [u8]) -> Result<Self, FieldParsingError> {
|
||||
let data = checked_len::<4>(data)?;
|
||||
Ok(Self {
|
||||
m_e_index: data[0],
|
||||
source_index: u16::from_be_bytes(data[2..=3].try_into().unwrap()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Field<'a>: Sized {
|
||||
const TYPE: &'static str;
|
||||
fn decode(data: &'a [u8]) -> Result<Self, FieldParsingError>;
|
||||
fn try_from_raw(raw: RawField<'a>) -> Result<Self, FieldParsingError> {
|
||||
if Self::TYPE != raw.r#type {
|
||||
Err(FieldParsingError::MismatchedFieldType)
|
||||
} else {
|
||||
Self::decode(&raw.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FieldParsingError {
|
||||
UnexpectedLength { expected: usize, got: usize },
|
||||
MismatchedFieldType,
|
||||
}
|
||||
|
||||
fn checked_len<'a, const LEN: usize>(data: &'a [u8]) -> Result<&'a [u8; LEN], FieldParsingError> {
|
||||
data.try_into()
|
||||
.map_err(|_| FieldParsingError::UnexpectedLength {
|
||||
expected: LEN,
|
||||
got: data.len(),
|
||||
})
|
||||
}
|
||||
@@ -1,314 +1,115 @@
|
||||
//! This module contains [`AtemPacket`] which is a zero*ish* copy abstraction over a [`AsRef`]`<[u8]>`.
|
||||
use core::{fmt::Display, str};
|
||||
use enumflags2::{BitFlags, bitflags};
|
||||
|
||||
use super::atem_field::{Field, FieldParsingError, RawField};
|
||||
|
||||
/// The "hello" packet to start communication with the ATEM
|
||||
pub const COMMAND_CONNECT_HELLO: [u8; 20] = [
|
||||
0x10, 0x14, 0x53, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
|
||||
/// Maximum atem packet length. This is determined by the maximum value that can be stored
|
||||
/// in the length field.
|
||||
pub const MAX_LEN: usize = field::LEN_MASK as usize;
|
||||
|
||||
mod field {
|
||||
use core::ops::{Range, RangeInclusive};
|
||||
|
||||
pub(crate) const FLAGS_LEN_H: usize = 0;
|
||||
pub(crate) const FLAGS_MASK: u8 = 0b1111_1000;
|
||||
pub(crate) const LEN_L: usize = 1;
|
||||
pub(crate) const LEN: RangeInclusive<usize> = FLAGS_LEN_H..=LEN_L;
|
||||
pub(crate) const LEN_MASK: u16 = 0x7ff;
|
||||
|
||||
pub(crate) const SESSION_ID: Range<usize> = 2..4;
|
||||
pub(crate) const ACK_NUMBER: Range<usize> = 4..6;
|
||||
pub(crate) const REMOTE_SEQ_NUM: Range<usize> = 8..10;
|
||||
pub(crate) const LOCAL_SEQ_NUM: Range<usize> = 10..12;
|
||||
}
|
||||
|
||||
/// An ATEM protocol packet
|
||||
#[derive(Debug)]
|
||||
pub struct AtemPacket<T: AsRef<[u8]>> {
|
||||
buf: T,
|
||||
pub struct AtemPacket<'packet_buffer> {
|
||||
flags: u8,
|
||||
session_id: u16,
|
||||
remote_packet_id: u16,
|
||||
retransmit_requested_from_packet_id: Option<u16>,
|
||||
ack_reply: Option<u16>,
|
||||
body: Option<&'packet_buffer [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a [u8]> for AtemPacket<&'a [u8]> {
|
||||
type Error = AtemPacketParseError;
|
||||
fn try_from(buf: &'a [u8]) -> Result<Self, Self::Error> {
|
||||
AtemPacket::new_checked(buf)
|
||||
}
|
||||
pub enum AtemPacketErr {
|
||||
TooShort(String),
|
||||
LengthDiffers(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum AtemPacketParseError {
|
||||
/// The packet was shorter than any valid packet should be.
|
||||
TooShort { got: usize },
|
||||
/// The packet's stated and actual lengths were different
|
||||
LengthDiffers { expected: u16, got: usize },
|
||||
/// The packet had invalid (unknown) flags set
|
||||
InvalidFlags,
|
||||
}
|
||||
|
||||
/// ATEM packet flags
|
||||
#[bitflags]
|
||||
#[repr(u8)]
|
||||
#[derive(PartialEq, Copy, Clone, Debug)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum PacketFlag {
|
||||
/// AKA "reliable". This packet should be ACKed by the other end
|
||||
AckRequest = 0x1,
|
||||
/// AKA "syn" - used in the connection handshake
|
||||
NewSessionId = 0x2,
|
||||
/// This packet is a retransmit of a previous one
|
||||
IsRetransmit = 0x4,
|
||||
/// Request retransmission of a previous sequence ID
|
||||
RetransmitRequest = 0x8,
|
||||
/// This packet is an ACK
|
||||
AckReply = 0x10,
|
||||
AckRequest,
|
||||
NewSessionId,
|
||||
IsRetransmit,
|
||||
RetransmitRequest,
|
||||
AckReply,
|
||||
}
|
||||
|
||||
/// An error while constructing a packet
|
||||
#[derive(Debug)]
|
||||
pub enum AtemPacketInitError {
|
||||
/// The provided buf was too small
|
||||
BufTooSmall { need: usize, got: usize },
|
||||
/// The provided payload was too long
|
||||
DataTooLong { got: usize, max: usize },
|
||||
}
|
||||
|
||||
impl<'a> AtemPacket<&'a mut [u8]> {
|
||||
/// Initialise an [`AtemPacket`] into the given `buf`, with optional payload copied from `data`.
|
||||
/// Returns the constructed packet, and the remaining spare space in `buf`, if there was any.
|
||||
pub fn init<'b>(
|
||||
buf: &'a mut [u8],
|
||||
flags: BitFlags<PacketFlag>,
|
||||
session_id: u16,
|
||||
local_seq_num: u16,
|
||||
data: Option<&'b [u8]>,
|
||||
) -> Result<(Self, &'a mut [u8]), AtemPacketInitError> {
|
||||
let len = 12 + data.map_or(0, |d| d.len());
|
||||
if len > buf.len() {
|
||||
return Err(AtemPacketInitError::BufTooSmall {
|
||||
need: len,
|
||||
got: buf.len(),
|
||||
});
|
||||
impl From<PacketFlag> for u8 {
|
||||
fn from(flag: PacketFlag) -> Self {
|
||||
match flag {
|
||||
PacketFlag::AckRequest => 0x01,
|
||||
PacketFlag::NewSessionId => 0x02,
|
||||
PacketFlag::IsRetransmit => 0x04,
|
||||
PacketFlag::RetransmitRequest => 0x08,
|
||||
PacketFlag::AckReply => 0x10,
|
||||
}
|
||||
if len > MAX_LEN {
|
||||
return Err(AtemPacketInitError::DataTooLong {
|
||||
got: len - 12,
|
||||
max: MAX_LEN - 12,
|
||||
});
|
||||
}
|
||||
let (pkt, rem) = buf.split_at_mut(len);
|
||||
let mut p = AtemPacket { buf: pkt };
|
||||
p.set_flags(flags)
|
||||
.set_session_id(session_id)
|
||||
.set_local_seq_num(local_seq_num)
|
||||
.set_len(len.try_into().unwrap());
|
||||
if let Some(d) = data {
|
||||
p.set_data(d);
|
||||
}
|
||||
Ok((p, rem))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]> + AsMut<[u8]>> AtemPacket<T> {
|
||||
pub fn set_flags(&mut self, flags: BitFlags<PacketFlag>) -> &mut Self {
|
||||
let prev = self.buf.as_ref()[field::FLAGS_LEN_H];
|
||||
self.buf.as_mut()[field::FLAGS_LEN_H] = (prev & !field::FLAGS_MASK) | (flags.bits() << 3);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_len(&mut self, value: u16) -> &mut Self {
|
||||
let v = value.to_be_bytes();
|
||||
self.buf.as_mut()[field::FLAGS_LEN_H] &= v[0] | field::FLAGS_MASK;
|
||||
self.buf.as_mut()[field::LEN_L] = v[1];
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_session_id(&mut self, value: u16) -> &mut Self {
|
||||
self.buf.as_mut()[field::SESSION_ID].copy_from_slice(&value.to_be_bytes());
|
||||
self
|
||||
}
|
||||
pub fn set_local_seq_num(&mut self, value: u16) -> &mut Self {
|
||||
self.buf.as_mut()[field::LOCAL_SEQ_NUM].copy_from_slice(&value.to_be_bytes());
|
||||
self
|
||||
}
|
||||
pub fn set_remote_seq_num(&mut self, value: u16) -> &mut Self {
|
||||
self.buf.as_mut()[field::REMOTE_SEQ_NUM].copy_from_slice(&value.to_be_bytes());
|
||||
self
|
||||
}
|
||||
pub fn set_data<'a>(&mut self, value: &'a [u8]) -> &mut Self {
|
||||
self.buf.as_mut()[12..].copy_from_slice(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_ack_num(&mut self, value: u16) -> &mut Self {
|
||||
self.buf.as_mut()[field::ACK_NUMBER].copy_from_slice(&value.to_be_bytes());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> AtemPacket<T> {
|
||||
pub fn new_checked(buf: T) -> Result<Self, AtemPacketParseError> {
|
||||
let len = buf.as_ref().len();
|
||||
if len < 12 {
|
||||
return Err(AtemPacketParseError::TooShort {
|
||||
got: buf.as_ref().len(),
|
||||
});
|
||||
}
|
||||
let p = Self { buf };
|
||||
if p.length() as usize != len {
|
||||
return Err(AtemPacketParseError::LengthDiffers {
|
||||
expected: p.length(),
|
||||
got: len,
|
||||
});
|
||||
}
|
||||
// Check flags are valid
|
||||
let _: BitFlags<PacketFlag> = p
|
||||
.flags_raw()
|
||||
.try_into()
|
||||
.map_err(|_| AtemPacketParseError::InvalidFlags)?;
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// Consumes self, returning the inner packet buffer
|
||||
pub fn inner(self) -> T {
|
||||
self.buf
|
||||
}
|
||||
pub fn length(&self) -> u16 {
|
||||
u16::from_be_bytes(self.buf.as_ref()[field::LEN].try_into().unwrap()) & field::LEN_MASK
|
||||
}
|
||||
|
||||
fn flags_raw(&self) -> u8 {
|
||||
self.buf.as_ref()[field::FLAGS_LEN_H] >> 3
|
||||
}
|
||||
|
||||
pub fn flags(&self) -> BitFlags<PacketFlag> {
|
||||
// We `unwrap` here, but given we check the flags in the constructor this should never panic.
|
||||
self.flags_raw().try_into().unwrap()
|
||||
}
|
||||
|
||||
impl<'packet_buffer> AtemPacket<'packet_buffer> {
|
||||
pub fn session_id(&self) -> u16 {
|
||||
u16::from_be_bytes(self.buf.as_ref()[field::SESSION_ID].try_into().unwrap())
|
||||
self.session_id
|
||||
}
|
||||
|
||||
pub fn ack_number(&self) -> u16 {
|
||||
u16::from_be_bytes(self.buf.as_ref()[field::ACK_NUMBER].try_into().unwrap())
|
||||
pub fn remote_packet_id(&self) -> u16 {
|
||||
self.remote_packet_id
|
||||
}
|
||||
|
||||
pub fn remote_sequence_number(&self) -> u16 {
|
||||
u16::from_be_bytes(self.buf.as_ref()[field::REMOTE_SEQ_NUM].try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn local_sequence_number(&self) -> u16 {
|
||||
u16::from_be_bytes(self.buf.as_ref()[field::LOCAL_SEQ_NUM].try_into().unwrap())
|
||||
pub fn body(&self) -> Option<&[u8]> {
|
||||
self.body
|
||||
}
|
||||
|
||||
pub fn retransmit_request(&self) -> Option<u16> {
|
||||
self.flags()
|
||||
.contains(PacketFlag::RetransmitRequest)
|
||||
.then_some(u16::from_be_bytes([
|
||||
self.buf.as_ref()[6],
|
||||
self.buf.as_ref()[7],
|
||||
]))
|
||||
self.retransmit_requested_from_packet_id
|
||||
}
|
||||
|
||||
pub fn ack_reply(&self) -> Option<u16> {
|
||||
self.flags()
|
||||
.contains(PacketFlag::AckReply)
|
||||
.then_some(self.ack_number())
|
||||
self.ack_reply
|
||||
}
|
||||
|
||||
/// Get an iterator over the `Field`s in this packet.
|
||||
///
|
||||
/// Returns None if this is a packet without fields.
|
||||
pub fn raw_fields(&self) -> RawFields {
|
||||
// TODO: do we only ever get newsessionid during the handshake (i.e. not in a packet with fields)?
|
||||
let has_fields = !self.flags().contains(PacketFlag::NewSessionId);
|
||||
RawFields::new(if has_fields { self.body() } else { &[] })
|
||||
}
|
||||
|
||||
pub fn fields<'a, F: Field<'a>>(
|
||||
&'a self,
|
||||
) -> impl Iterator<Item = Result<F, FieldParsingError>> + use<'a, F, T> {
|
||||
self.raw_fields().filter_map(|f| match F::try_from_raw(f) {
|
||||
Err(FieldParsingError::MismatchedFieldType) => None,
|
||||
x => Some(x),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn body(&self) -> &[u8] {
|
||||
&self.buf.as_ref()[12..]
|
||||
pub fn has_flag(&self, flag: PacketFlag) -> bool {
|
||||
self.flags & u8::from(flag) > 0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> Display for AtemPacket<T> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"len: {}, sid: {}, flags: {}, ack: {}, localseq: {}, remoteseq: {}",
|
||||
self.length(),
|
||||
self.session_id(),
|
||||
self.flags(),
|
||||
self.ack_number(),
|
||||
self.local_sequence_number(),
|
||||
self.remote_sequence_number(),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl<'packet_buffer> TryFrom<&'packet_buffer [u8]> for AtemPacket<'packet_buffer> {
|
||||
type Error = AtemPacketErr;
|
||||
|
||||
/// Created by [`AtemPacket::raw_fields`]
|
||||
pub struct RawFields<'a> {
|
||||
data: &'a [u8],
|
||||
// The offset of the next field in the packet
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> RawFields<'a> {
|
||||
pub(crate) fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, offset: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RawFields<'a> {
|
||||
type Item = RawField<'a>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let remain = self.data.len() - self.offset;
|
||||
if remain == 0 {
|
||||
return None;
|
||||
} else if remain < 8 {
|
||||
// TODO: is 8 indeed the minimum size for something here? (i.e. no field data)
|
||||
panic!("Oh no");
|
||||
fn try_from(buffer: &'packet_buffer [u8]) -> Result<Self, Self::Error> {
|
||||
if buffer.len() < 12 {
|
||||
return Err(AtemPacketErr::TooShort(format!(
|
||||
"Invalid packet from ATEM {:x?}",
|
||||
buffer
|
||||
)));
|
||||
}
|
||||
|
||||
let length =
|
||||
u16::from_be_bytes(self.data[self.offset..=self.offset + 1].try_into().unwrap());
|
||||
// TODO: sanity check length
|
||||
let r#type = str::from_utf8(&self.data[self.offset + 4..=self.offset + 7]).unwrap();
|
||||
let data = &self.data[self.offset + 8..self.offset + (length as usize)];
|
||||
self.offset += length as usize;
|
||||
Some(RawField { r#type, data })
|
||||
}
|
||||
}
|
||||
let length = u16::from_be_bytes([buffer[0], buffer[1]]) & 0x07ff;
|
||||
if length as usize != buffer.len() {
|
||||
return Err(AtemPacketErr::LengthDiffers(format!(
|
||||
"Length of message differs, expected {} got {}",
|
||||
length,
|
||||
buffer.len()
|
||||
)));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
let flags = buffer[0] >> 3;
|
||||
let session_id = u16::from_be_bytes([buffer[2], buffer[3]]);
|
||||
let remote_packet_id = u16::from_be_bytes([buffer[10], buffer[11]]);
|
||||
|
||||
#[test]
|
||||
fn test_init() {
|
||||
let mut buf = [0u8; 12];
|
||||
let (mut p, _) =
|
||||
AtemPacket::init(&mut buf, PacketFlag::AckReply.into(), 1234, 5678, None).unwrap();
|
||||
p.set_remote_seq_num(9012);
|
||||
assert!(p.flags().contains(PacketFlag::AckReply));
|
||||
assert_eq!(p.session_id(), 1234);
|
||||
assert_eq!(p.local_sequence_number(), 5678);
|
||||
assert_eq!(p.remote_sequence_number(), 9012);
|
||||
assert_eq!(p.length(), 12);
|
||||
let body = if buffer.len() > 12 {
|
||||
Some(&buffer[12..])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let retransmit_requested_from_packet_id =
|
||||
if flags & u8::from(PacketFlag::RetransmitRequest) > 0 {
|
||||
Some(u16::from_be_bytes([buffer[6], buffer[7]]))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ack_reply = if flags & u8::from(PacketFlag::AckReply) > 0 {
|
||||
Some(u16::from_be_bytes([buffer[4], buffer[5]]))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(AtemPacket {
|
||||
flags,
|
||||
session_id,
|
||||
remote_packet_id,
|
||||
body,
|
||||
retransmit_requested_from_packet_id,
|
||||
ack_reply,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
use std::{
|
||||
borrow::ToOwned,
|
||||
collections::VecDeque,
|
||||
fmt::Display,
|
||||
io,
|
||||
net::SocketAddr,
|
||||
string::{String, ToString},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
vec,
|
||||
vec::Vec,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
use tokio::{
|
||||
net::UdpSocket,
|
||||
select,
|
||||
@@ -18,17 +14,20 @@ use tokio::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
atem_lib::atem_packet::{AtemPacket, COMMAND_CONNECT_HELLO},
|
||||
commands::command_base::{BasicWritableCommand, DeserializedCommand},
|
||||
atem_lib::{atem_packet::AtemPacket, atem_util},
|
||||
commands::{
|
||||
command_base::{BasicWritableCommand, DeserializedCommand},
|
||||
parse_commands::deserialize_commands,
|
||||
},
|
||||
enums::ProtocolVersion,
|
||||
};
|
||||
|
||||
use super::atem_packet::PacketFlag;
|
||||
|
||||
const IN_FLIGHT_TIMEOUT: Duration = Duration::from_millis(60);
|
||||
const CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000);
|
||||
const CONNECTION_RETRY_INTERVAL: Duration = Duration::from_millis(1000);
|
||||
const RETRANSMIT_CHECK_INTERVAL: Duration = Duration::from_millis(1000);
|
||||
const IN_FLIGHT_TIMEOUT: u64 = 60;
|
||||
const CONNECTION_TIMEOUT: u64 = 5000;
|
||||
const CONNECTION_RETRY_INTERVAL: u64 = 1000;
|
||||
const RETRANSMIT_CHECK_INTERVAL: u64 = 1000;
|
||||
const MAX_PACKET_RETRIES: u16 = 10;
|
||||
const MAX_PACKET_ID: u16 = 1 << 15;
|
||||
const MAX_PACKET_PER_ACK: u16 = 16;
|
||||
@@ -55,10 +54,10 @@ pub struct TrackingIdsCallback {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AtemSocketEvent {
|
||||
pub enum AtemEvent {
|
||||
Connected,
|
||||
Disconnected,
|
||||
ReceivedCommands(Vec<u8>),
|
||||
ReceivedCommands(VecDeque<Arc<dyn DeserializedCommand>>),
|
||||
AckedCommand(TrackingId),
|
||||
}
|
||||
|
||||
@@ -83,7 +82,7 @@ pub struct AtemSocketCommand {
|
||||
}
|
||||
|
||||
impl AtemSocketCommand {
|
||||
pub fn new<C: BasicWritableCommand>(command: &C, version: &ProtocolVersion) -> Self {
|
||||
pub fn new(command: &Box<dyn BasicWritableCommand>, version: &ProtocolVersion) -> Self {
|
||||
Self {
|
||||
payload: command.payload(version),
|
||||
raw_name: command.get_raw_name().to_string(),
|
||||
@@ -93,8 +92,8 @@ impl AtemSocketCommand {
|
||||
|
||||
pub struct AtemSocket {
|
||||
connection_state: ConnectionState,
|
||||
reconnect_timer: Option<Instant>,
|
||||
retransmit_timer: Option<Instant>,
|
||||
reconnect_timer: Option<SystemTime>,
|
||||
retransmit_timer: Option<SystemTime>,
|
||||
|
||||
next_tracking_id: u64,
|
||||
|
||||
@@ -104,19 +103,14 @@ pub struct AtemSocket {
|
||||
socket: Option<UdpSocket>,
|
||||
address: SocketAddr,
|
||||
|
||||
protocol_version: ProtocolVersion,
|
||||
|
||||
last_received_at: Instant,
|
||||
last_received_at: SystemTime,
|
||||
last_received_packed_id: u16,
|
||||
in_flight: Vec<InFlightPacket>,
|
||||
ack_timer: Option<Instant>,
|
||||
ack_timer: Option<SystemTime>,
|
||||
received_without_ack: u16,
|
||||
|
||||
atem_message_rx: tokio::sync::mpsc::Receiver<AtemSocketMessage>,
|
||||
atem_event_tx: tokio::sync::mpsc::UnboundedSender<AtemSocketEvent>,
|
||||
atem_event_tx: tokio::sync::mpsc::UnboundedSender<AtemEvent>,
|
||||
connected_callbacks: Mutex<Vec<tokio::sync::oneshot::Sender<bool>>>,
|
||||
|
||||
tick_interval: tokio::time::Interval,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
@@ -142,7 +136,7 @@ struct InFlightPacket {
|
||||
packet_id: u16,
|
||||
tracking_id: u64,
|
||||
payload: Vec<u8>,
|
||||
pub last_sent: Instant,
|
||||
pub last_sent: SystemTime,
|
||||
pub resent: u16,
|
||||
}
|
||||
|
||||
@@ -151,11 +145,7 @@ enum AtemSocketReceiveError {
|
||||
}
|
||||
|
||||
impl AtemSocket {
|
||||
pub fn new(
|
||||
atem_message_rx: tokio::sync::mpsc::Receiver<AtemSocketMessage>,
|
||||
atem_event_tx: tokio::sync::mpsc::UnboundedSender<AtemSocketEvent>,
|
||||
) -> Self {
|
||||
let tick_interval = tokio::time::interval(Duration::from_millis(5));
|
||||
pub fn new(atem_event_tx: tokio::sync::mpsc::UnboundedSender<AtemEvent>) -> Self {
|
||||
Self {
|
||||
connection_state: ConnectionState::Closed,
|
||||
reconnect_timer: None,
|
||||
@@ -169,80 +159,80 @@ impl AtemSocket {
|
||||
socket: None,
|
||||
address: "0.0.0.0:0".parse().unwrap(),
|
||||
|
||||
protocol_version: ProtocolVersion::V7_2,
|
||||
|
||||
last_received_at: Instant::now(),
|
||||
last_received_at: SystemTime::now(),
|
||||
last_received_packed_id: 0,
|
||||
in_flight: vec![],
|
||||
ack_timer: None,
|
||||
received_without_ack: 0,
|
||||
|
||||
atem_message_rx,
|
||||
atem_event_tx,
|
||||
connected_callbacks: Mutex::default(),
|
||||
|
||||
tick_interval,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn poll(&mut self) {
|
||||
let tick = self.tick_interval.tick();
|
||||
select! {
|
||||
_ = tick => {},
|
||||
message = self.atem_message_rx.recv() => {
|
||||
match message {
|
||||
Some(AtemSocketMessage::Connect {
|
||||
address,
|
||||
result_callback,
|
||||
}) => {
|
||||
{
|
||||
let mut connected_callbacks = self.connected_callbacks.lock().await;
|
||||
connected_callbacks.push(result_callback);
|
||||
}
|
||||
if self.connect(address).await.is_err() {
|
||||
log::debug!("Connect failed");
|
||||
let mut connected_callbacks = self.connected_callbacks.lock().await;
|
||||
for callback in connected_callbacks.drain(0..) {
|
||||
let _ = callback.send(false);
|
||||
pub async fn run(
|
||||
&mut self,
|
||||
mut atem_message_rx: tokio::sync::mpsc::Receiver<AtemSocketMessage>,
|
||||
cancel: tokio_util::sync::CancellationToken,
|
||||
) {
|
||||
while !cancel.is_cancelled() {
|
||||
select! {
|
||||
_ = cancel.cancelled() => {},
|
||||
message = atem_message_rx.recv() => {
|
||||
match message {
|
||||
Some(AtemSocketMessage::Connect {
|
||||
address,
|
||||
result_callback,
|
||||
}) => {
|
||||
{
|
||||
let mut connected_callbacks = self.connected_callbacks.lock().await;
|
||||
connected_callbacks.push(result_callback);
|
||||
}
|
||||
if self.connect(address).await.is_err() {
|
||||
let mut connected_callbacks = self.connected_callbacks.lock().await;
|
||||
for callback in connected_callbacks.drain(0..) {
|
||||
let _ = callback.send(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(AtemSocketMessage::Disconnect) => self.disconnect(),
|
||||
Some(AtemSocketMessage::SendCommands {
|
||||
commands,
|
||||
tracking_ids_callback,
|
||||
}) => {
|
||||
let barrier = Arc::new(Barrier::new(2));
|
||||
tracking_ids_callback
|
||||
.send(TrackingIdsCallback {
|
||||
tracking_ids: self.send_commands(commands).await,
|
||||
barrier: barrier.clone(),
|
||||
})
|
||||
.ok();
|
||||
Some(AtemSocketMessage::Disconnect) => self.disconnect(),
|
||||
Some(AtemSocketMessage::SendCommands {
|
||||
commands,
|
||||
tracking_ids_callback,
|
||||
}) => {
|
||||
let barrier = Arc::new(Barrier::new(2));
|
||||
tracking_ids_callback
|
||||
.send(TrackingIdsCallback {
|
||||
tracking_ids: self.send_commands(commands).await,
|
||||
barrier: barrier.clone(),
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Let's play the game "Synchronisation Shenanigans"!
|
||||
// So, we are sending tracking Ids to the sender of this message, the sender will then wait
|
||||
// for each of these tracking Ids to be ACK'd by the ATEM. However, the sender will need to
|
||||
// do ✨ some form of shenanigans ✨ in order to be ready to receive tracking Ids. So we send
|
||||
// them a barrier as part of the callback so that they can tell us that they are ready for
|
||||
// us to continue with ATEM communication, at which point we may immediately inform them of a
|
||||
// received tracking Id matching one included in this callback.
|
||||
//
|
||||
// Now, if we were being 🚩 Real Proper Software Developers 🚩 we'd probably expect the receiver
|
||||
// of the callback to do clever things so that if a tracking Id is received immediately, they
|
||||
// then wait for something that wants that tracking Id on their side, rather than blocking this
|
||||
// task so that the caller can do ✨ shenanigans ✨. However, that sounds far too clever and too
|
||||
// much like 🚩 Real Actual Work 🚩 so instead we've chosen to do this and hope that whichever
|
||||
// actor we're waiting on doesn't take _too_ long to do ✨ shenanigans ✨ before signalling that
|
||||
// they are ready. If they do, I suggest finding whoever wrote that code and bonking them 🔨.
|
||||
barrier.wait().await;
|
||||
},
|
||||
None => {
|
||||
log::info!("ATEM message channel has closed.");
|
||||
// Let's play the game "Synchronisation Shenanigans"!
|
||||
// So, we are sending tracking Ids to the sender of this message, the sender will then wait
|
||||
// for each of these tracking Ids to be ACK'd by the ATEM. However, the sender will need to
|
||||
// do ✨ some form of shenanigans ✨ in order to be ready to receive tracking Ids. So we send
|
||||
// them a barrier as part of the callback so that they can tell us that they are ready for
|
||||
// us to continue with ATEM communication, at which point we may immediately inform them of a
|
||||
// received tracking Id matching one included in this callback.
|
||||
//
|
||||
// Now, if we were being 🚩 Real Proper Software Developers 🚩 we'd probably expect the receiver
|
||||
// of the callback to do clever things so that if a tracking Id is received immediately, they
|
||||
// then wait for something that wants that tracking Id on their side, rather than blocking this
|
||||
// task so that the caller can do ✨ shenanigans ✨. However, that sounds far too clever and too
|
||||
// much like 🚩 Real Actual Work 🚩 so instead we've chosen to do this and hope that whichever
|
||||
// actor we're waiting on doesn't take _too_ long to do ✨ shenanigans ✨ before signalling that
|
||||
// they are ready. If they do, I suggest finding whoever wrote that code and bonking them 🔨.
|
||||
barrier.wait().await;
|
||||
},
|
||||
None => {
|
||||
log::info!("ATEM message channel has closed, exiting event loop.");
|
||||
cancel.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
self.tick().await;
|
||||
}
|
||||
@@ -259,7 +249,7 @@ impl AtemSocket {
|
||||
self.in_flight = vec![];
|
||||
log::debug!("Reconnect");
|
||||
|
||||
self.send_packet(&COMMAND_CONNECT_HELLO).await;
|
||||
self.send_packet(&atem_util::COMMAND_CONNECT_HELLO).await;
|
||||
self.connection_state = ConnectionState::SynSent;
|
||||
|
||||
Ok(())
|
||||
@@ -300,24 +290,28 @@ impl AtemSocket {
|
||||
self.next_send_packet_id = 0;
|
||||
}
|
||||
|
||||
let opcode = u16::from(u8::from(PacketFlag::AckRequest)) << 11;
|
||||
|
||||
let mut buffer = vec![0; 20 + payload.len()];
|
||||
|
||||
let (p, _) = AtemPacket::init(
|
||||
&mut buffer,
|
||||
PacketFlag::AckRequest.into(),
|
||||
self.session_id,
|
||||
packet_id,
|
||||
Some([raw_name.as_bytes(), payload].concat().as_slice()),
|
||||
)
|
||||
.unwrap();
|
||||
// Headers
|
||||
buffer[0..2].copy_from_slice(&u16::to_be_bytes(opcode | (payload.len() as u16 + 20)));
|
||||
buffer[2..4].copy_from_slice(&u16::to_be_bytes(self.session_id));
|
||||
buffer[10..12].copy_from_slice(&u16::to_be_bytes(packet_id));
|
||||
|
||||
self.send_packet(&p.inner()).await;
|
||||
// Command
|
||||
buffer[12..14].copy_from_slice(&u16::to_be_bytes(payload.len() as u16 + 8));
|
||||
buffer[16..20].copy_from_slice(raw_name.as_bytes());
|
||||
|
||||
// Body
|
||||
buffer[20..20 + payload.len()].copy_from_slice(payload);
|
||||
self.send_packet(&buffer).await;
|
||||
|
||||
self.in_flight.push(InFlightPacket {
|
||||
packet_id,
|
||||
tracking_id,
|
||||
payload: buffer,
|
||||
last_sent: Instant::now(),
|
||||
last_sent: SystemTime::now(),
|
||||
resent: 0,
|
||||
})
|
||||
}
|
||||
@@ -335,15 +329,17 @@ impl AtemSocket {
|
||||
}
|
||||
}
|
||||
if let Some(ack_time) = self.ack_timer {
|
||||
if ack_time <= Instant::now() {
|
||||
if ack_time <= SystemTime::now() {
|
||||
self.ack_timer = None;
|
||||
self.received_without_ack = 0;
|
||||
self.send_ack(self.last_received_packed_id).await;
|
||||
}
|
||||
}
|
||||
if let Some(reconnect_time) = self.reconnect_timer {
|
||||
if reconnect_time <= Instant::now() {
|
||||
if self.last_received_at + CONNECTION_TIMEOUT <= Instant::now() {
|
||||
if reconnect_time <= SystemTime::now() {
|
||||
if self.last_received_at + Duration::from_millis(CONNECTION_TIMEOUT)
|
||||
<= SystemTime::now()
|
||||
{
|
||||
log::debug!("{:?}", self.last_received_at);
|
||||
log::debug!("Connection timed out, restarting");
|
||||
self.restart_connection().await;
|
||||
@@ -352,7 +348,7 @@ impl AtemSocket {
|
||||
}
|
||||
}
|
||||
if let Some(retransmit_time) = self.retransmit_timer {
|
||||
if retransmit_time <= Instant::now() {
|
||||
if retransmit_time <= SystemTime::now() {
|
||||
self.check_for_retransmit().await;
|
||||
self.start_retransmit_timer();
|
||||
}
|
||||
@@ -381,28 +377,23 @@ impl AtemSocket {
|
||||
}
|
||||
|
||||
async fn recieved_packet(&mut self, packet: &[u8]) {
|
||||
let Ok(atem_packet): Result<AtemPacket<_>, _> = packet.try_into() else {
|
||||
let Ok(atem_packet): Result<AtemPacket, _> = packet.try_into() else {
|
||||
return;
|
||||
};
|
||||
|
||||
log::debug!("Received {}", atem_packet,);
|
||||
log::debug!(
|
||||
"fields: {}",
|
||||
atem_packet.raw_fields().map(|f| f.r#type).join(",")
|
||||
);
|
||||
log::debug!("Received {:x?}", atem_packet);
|
||||
|
||||
self.last_received_at = Instant::now();
|
||||
self.last_received_at = SystemTime::now();
|
||||
|
||||
self.session_id = atem_packet.session_id();
|
||||
// TODO: bit of a naming clash here
|
||||
let remote_packet_id = atem_packet.local_sequence_number();
|
||||
let remote_packet_id = atem_packet.remote_packet_id();
|
||||
|
||||
if atem_packet.flags().contains(PacketFlag::NewSessionId) {
|
||||
if atem_packet.has_flag(PacketFlag::NewSessionId) {
|
||||
log::debug!("New session");
|
||||
self.connection_state = ConnectionState::Established;
|
||||
self.last_received_packed_id = remote_packet_id;
|
||||
self.send_ack(remote_packet_id).await;
|
||||
self.on_connect().await;
|
||||
self.on_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -413,12 +404,14 @@ impl AtemSocket {
|
||||
self.retransmit_from(from_packet_id).await;
|
||||
}
|
||||
|
||||
if atem_packet.flags().contains(PacketFlag::AckRequest) {
|
||||
if atem_packet.has_flag(PacketFlag::AckRequest) {
|
||||
if remote_packet_id == (self.last_received_packed_id + 1) % MAX_PACKET_ID {
|
||||
self.last_received_packed_id = remote_packet_id;
|
||||
self.send_or_queue_ack().await;
|
||||
|
||||
self.on_commands_received(atem_packet.body());
|
||||
if let Some(body) = atem_packet.body() {
|
||||
self.on_commands_received(body);
|
||||
}
|
||||
} else if self
|
||||
.is_packet_covered_by_ack(self.last_received_packed_id, remote_packet_id)
|
||||
{
|
||||
@@ -426,7 +419,7 @@ impl AtemSocket {
|
||||
}
|
||||
}
|
||||
|
||||
if atem_packet.flags().contains(PacketFlag::IsRetransmit) {
|
||||
if atem_packet.has_flag(PacketFlag::IsRetransmit) {
|
||||
log::debug!("ATEM retransmitted packet {:x?}", remote_packet_id);
|
||||
}
|
||||
|
||||
@@ -470,23 +463,19 @@ impl AtemSocket {
|
||||
self.ack_timer = None;
|
||||
self.send_ack(self.last_received_packed_id).await;
|
||||
} else if self.ack_timer.is_none() {
|
||||
self.ack_timer = Some(Instant::now() + Duration::from_millis(5));
|
||||
self.ack_timer = Some(SystemTime::now() + Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_ack(&mut self, packet_id: u16) {
|
||||
log::debug!("Sending ack for packet {:x?}", packet_id);
|
||||
let flag: u8 = PacketFlag::AckReply.into();
|
||||
let opcode = u16::from(flag) << 11;
|
||||
let mut buffer: [u8; ACK_PACKET_LENGTH as _] = [0; 12];
|
||||
let (mut p, _) = AtemPacket::init(
|
||||
&mut buffer,
|
||||
PacketFlag::AckReply.into(),
|
||||
self.session_id,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
p.set_ack_num(packet_id);
|
||||
self.send_packet(p.inner()).await;
|
||||
buffer[0..2].copy_from_slice(&u16::to_be_bytes(opcode | ACK_PACKET_LENGTH));
|
||||
buffer[2..4].copy_from_slice(&u16::to_be_bytes(self.session_id));
|
||||
buffer[4..6].copy_from_slice(&u16::to_be_bytes(packet_id));
|
||||
self.send_packet(&buffer).await;
|
||||
}
|
||||
|
||||
async fn retransmit_from(&mut self, from_id: u16) {
|
||||
@@ -507,7 +496,7 @@ impl AtemSocket {
|
||||
if sent_packet.packet_id == from_id
|
||||
|| !self.is_packet_covered_by_ack(from_id, sent_packet.packet_id)
|
||||
{
|
||||
sent_packet.last_sent = Instant::now();
|
||||
sent_packet.last_sent = SystemTime::now();
|
||||
sent_packet.resent += 1;
|
||||
|
||||
self.send_packet(&sent_packet.payload).await;
|
||||
@@ -521,7 +510,8 @@ impl AtemSocket {
|
||||
|
||||
async fn check_for_retransmit(&mut self) {
|
||||
for sent_packet in self.in_flight.clone() {
|
||||
if sent_packet.last_sent + IN_FLIGHT_TIMEOUT < Instant::now() {
|
||||
if sent_packet.last_sent + Duration::from_millis(IN_FLIGHT_TIMEOUT) < SystemTime::now()
|
||||
{
|
||||
if sent_packet.resent <= MAX_PACKET_RETRIES
|
||||
&& self
|
||||
.is_packet_covered_by_ack(self.next_send_packet_id, sent_packet.packet_id)
|
||||
@@ -538,35 +528,34 @@ impl AtemSocket {
|
||||
}
|
||||
|
||||
fn on_commands_received(&mut self, payload: &[u8]) {
|
||||
if !payload.is_empty() {
|
||||
let _ = self
|
||||
.atem_event_tx
|
||||
.send(AtemSocketEvent::ReceivedCommands(payload.to_vec()));
|
||||
}
|
||||
let commands = deserialize_commands(payload);
|
||||
|
||||
let _ = self
|
||||
.atem_event_tx
|
||||
.send(AtemEvent::ReceivedCommands(commands));
|
||||
}
|
||||
|
||||
fn on_command_acknowledged(&mut self, packets: Vec<AckedPacket>) {
|
||||
for ack in packets {
|
||||
let _ = self
|
||||
.atem_event_tx
|
||||
.send(AtemSocketEvent::AckedCommand(TrackingId(ack.tracking_id)));
|
||||
.send(AtemEvent::AckedCommand(TrackingId(ack.tracking_id)));
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_connect(&mut self) {
|
||||
let _ = self.atem_event_tx.send(AtemSocketEvent::Connected);
|
||||
let mut connected_callbacks = self.connected_callbacks.lock().await;
|
||||
fn on_connect(&mut self) {
|
||||
let _ = self.atem_event_tx.send(AtemEvent::Connected);
|
||||
let mut connected_callbacks = self.connected_callbacks.blocking_lock();
|
||||
for callback in connected_callbacks.drain(0..) {
|
||||
let _ = callback.send(false);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_disconnect(&mut self) {
|
||||
let _ = self.atem_event_tx.send(AtemSocketEvent::Disconnected);
|
||||
let _ = self.atem_event_tx.send(AtemEvent::Disconnected);
|
||||
}
|
||||
|
||||
fn start_timers(&mut self) {
|
||||
log::debug!("Starting timers");
|
||||
self.start_reconnect_timer();
|
||||
self.start_retransmit_timer();
|
||||
}
|
||||
@@ -577,11 +566,13 @@ impl AtemSocket {
|
||||
}
|
||||
|
||||
fn start_reconnect_timer(&mut self) {
|
||||
self.reconnect_timer = Some(Instant::now() + CONNECTION_RETRY_INTERVAL);
|
||||
self.reconnect_timer =
|
||||
Some(SystemTime::now() + Duration::from_millis(CONNECTION_RETRY_INTERVAL));
|
||||
}
|
||||
|
||||
fn start_retransmit_timer(&mut self) {
|
||||
self.retransmit_timer = Some(Instant::now() + RETRANSMIT_CHECK_INTERVAL);
|
||||
self.retransmit_timer =
|
||||
Some(SystemTime::now() + Duration::from_millis(RETRANSMIT_CHECK_INTERVAL));
|
||||
}
|
||||
|
||||
fn next_packet_tracking_id(&mut self) -> u64 {
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
use core::net::{Ipv4Addr, SocketAddrV4};
|
||||
|
||||
use log::debug;
|
||||
|
||||
use super::atem_packet::{AtemPacket, AtemPacketParseError, PacketFlag};
|
||||
|
||||
pub const ATEM_PORT: u16 = 9910;
|
||||
pub const SYN_PAYLOAD: [u8; 8] = [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum AtemSocketState {
|
||||
Closed,
|
||||
StateDump,
|
||||
Established { session_id: u16 },
|
||||
}
|
||||
|
||||
/// An abstraction over an async UDP socket
|
||||
pub trait SocketBackend {
|
||||
type SendError;
|
||||
type RecvError;
|
||||
async fn send_to(&mut self, data: &[u8], addr: SocketAddrV4) -> Result<(), Self::SendError>;
|
||||
async fn recv(&mut self, data: &mut [u8]) -> Result<usize, Self::RecvError>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub mod tokio {
|
||||
use core::net::Ipv4Addr;
|
||||
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
use super::{AtemSocket, SocketBackend};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TokioBackend {
|
||||
sock: UdpSocket,
|
||||
}
|
||||
|
||||
impl TokioBackend {
|
||||
pub async fn new(addr: Ipv4Addr) -> Result<AtemSocket<Self>, std::io::Error> {
|
||||
let b = Self {
|
||||
sock: UdpSocket::bind("0.0.0.0:0").await?,
|
||||
};
|
||||
Ok(AtemSocket::new_with_backend(addr, b))
|
||||
}
|
||||
}
|
||||
|
||||
impl SocketBackend for TokioBackend {
|
||||
type SendError = std::io::Error;
|
||||
type RecvError = std::io::Error;
|
||||
|
||||
async fn send_to(
|
||||
&mut self,
|
||||
data: &[u8],
|
||||
addr: core::net::SocketAddrV4,
|
||||
) -> Result<(), Self::SendError> {
|
||||
self.sock.send_to(data, addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv(&mut self, data: &mut [u8]) -> Result<usize, Self::RecvError> {
|
||||
let (cnt, _) = self.sock.recv_from(data).await?;
|
||||
Ok(cnt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "embassy-net")]
|
||||
pub mod enet {
|
||||
use core::net::{Ipv4Addr, SocketAddrV4};
|
||||
|
||||
use embassy_net::udp::{RecvError, SendError, UdpSocket};
|
||||
|
||||
use super::{AtemSocket, SocketBackend};
|
||||
|
||||
pub struct EnetBackend<'a> {
|
||||
sock: UdpSocket<'a>,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for EnetBackend<'_> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "EnetBackend")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> EnetBackend<'a> {
|
||||
pub fn new(addr: Ipv4Addr, sock: UdpSocket<'a>) -> AtemSocket<Self> {
|
||||
let b = Self { sock };
|
||||
AtemSocket::new_with_backend(addr, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl SocketBackend for EnetBackend<'_> {
|
||||
type SendError = SendError;
|
||||
type RecvError = RecvError;
|
||||
|
||||
async fn send_to(
|
||||
&mut self,
|
||||
data: &[u8],
|
||||
addr: SocketAddrV4,
|
||||
) -> Result<(), Self::SendError> {
|
||||
self.sock.send_to(data, addr).await
|
||||
}
|
||||
|
||||
async fn recv(&mut self, data: &mut [u8]) -> Result<usize, Self::RecvError> {
|
||||
let (count, _) = self.sock.recv_from(data).await?;
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum AtemError<I: SocketBackend> {
|
||||
/// Tried to do something but the socket is closed
|
||||
SocketClosed,
|
||||
/// There was an error from the underlying socket while receiving
|
||||
Recv(I::RecvError),
|
||||
/// There was an error from the underlying socket while sending
|
||||
Send(I::SendError),
|
||||
/// We received an invalid ATEM protocol packet
|
||||
BadPacket(AtemPacketParseError),
|
||||
/// The switcher denied our connection attempt.
|
||||
SwitcherDeniedConnect,
|
||||
/// There was some nonspecific protocol error
|
||||
ProtocolError,
|
||||
}
|
||||
|
||||
impl<I> From<AtemPacketParseError> for AtemError<I>
|
||||
where
|
||||
I: SocketBackend,
|
||||
{
|
||||
fn from(value: AtemPacketParseError) -> Self {
|
||||
Self::BadPacket(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AtemSocket<I: SocketBackend> {
|
||||
inner: I,
|
||||
addr: SocketAddrV4,
|
||||
state: AtemSocketState,
|
||||
local_seq_num: u16,
|
||||
}
|
||||
|
||||
impl<I> AtemSocket<I>
|
||||
where
|
||||
I: SocketBackend,
|
||||
{
|
||||
fn new_with_backend(addr: Ipv4Addr, backend: I) -> Self {
|
||||
Self {
|
||||
inner: backend,
|
||||
addr: SocketAddrV4::new(addr, ATEM_PORT),
|
||||
state: AtemSocketState::Closed,
|
||||
local_seq_num: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the ATEM, performing the connection handshake.
|
||||
pub async fn connect(&mut self) -> Result<(), AtemError<I>> {
|
||||
// Reset state
|
||||
self.state = AtemSocketState::Closed;
|
||||
self.local_seq_num = 0;
|
||||
let mut buf = [0u8; 20];
|
||||
// FIXME: this should be random
|
||||
let initial_sid = 0x53ab;
|
||||
|
||||
// Step 1: Send the "SYN" with a random session id and magic payload
|
||||
|
||||
let (mut p, _) = AtemPacket::init(
|
||||
&mut buf,
|
||||
PacketFlag::NewSessionId.into(),
|
||||
initial_sid,
|
||||
self.local_seq_num,
|
||||
Some(&SYN_PAYLOAD),
|
||||
)
|
||||
.unwrap();
|
||||
p.set_remote_seq_num(0x3a);
|
||||
|
||||
self.inner
|
||||
.send_to(p.inner(), self.addr)
|
||||
.await
|
||||
.map_err(|e| AtemError::Send(e))?;
|
||||
|
||||
debug!("Connect step 1: sent SYN");
|
||||
|
||||
// Step 2: Wait for the response from the ATEM.
|
||||
// this should also have the SYN flag set and have the same session id that we sent.
|
||||
// The first byte of the payload should be 0x02 - if it's 0x04 we should restart
|
||||
// the connection
|
||||
|
||||
self.inner
|
||||
.recv(&mut buf)
|
||||
.await
|
||||
.map_err(|e| AtemError::Recv(e))?;
|
||||
|
||||
let ack = AtemPacket::new_checked(buf)?;
|
||||
debug!("RX: {}", ack);
|
||||
if ack.flags() != PacketFlag::NewSessionId {
|
||||
return Err(AtemError::ProtocolError);
|
||||
}
|
||||
if ack.session_id() != initial_sid {
|
||||
return Err(AtemError::ProtocolError);
|
||||
}
|
||||
match ack.body()[0] {
|
||||
0x02 => {
|
||||
// all good
|
||||
debug!("Connect step 2: got reply");
|
||||
}
|
||||
0x04 => {
|
||||
// Restart connection
|
||||
return Err(AtemError::SwitcherDeniedConnect);
|
||||
}
|
||||
_ => {
|
||||
return Err(AtemError::ProtocolError);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Send an ACK for the packet we just received.
|
||||
self.ack(initial_sid, ack.local_sequence_number()).await?;
|
||||
debug!("Connect step 3: sent ACK");
|
||||
self.state = AtemSocketState::StateDump;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ack(&mut self, sid: u16, ack_num: u16) -> Result<(), AtemError<I>> {
|
||||
let mut buf = [0u8; 12];
|
||||
let (mut p, _) = AtemPacket::init(
|
||||
&mut buf,
|
||||
PacketFlag::AckReply.into(),
|
||||
sid,
|
||||
// N.B: local seq num is set to 0 in ACKs
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
p.set_ack_num(ack_num);
|
||||
debug!("TX: {}", p);
|
||||
self.inner
|
||||
.send_to(p.inner(), self.addr)
|
||||
.await
|
||||
.map_err(|e| AtemError::Send(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive a packet from the ATEM. Automatically handles sending ACKs for received packets.
|
||||
pub async fn recv<'a>(
|
||||
&mut self,
|
||||
buf: &'a mut [u8],
|
||||
) -> Result<AtemPacket<&'a [u8]>, AtemError<I>> {
|
||||
if self.state == AtemSocketState::Closed {
|
||||
return Err(AtemError::SocketClosed);
|
||||
}
|
||||
let size = self.inner.recv(buf).await.map_err(|e| AtemError::Recv(e))?;
|
||||
let p: AtemPacket<_> = buf[0..size].try_into()?;
|
||||
debug!("RX: {}", p);
|
||||
match self.state {
|
||||
AtemSocketState::StateDump => {
|
||||
// We've just connected and the state's being dumped to us.
|
||||
if p.body().len() == 0 {
|
||||
// State dump is done.
|
||||
self.state = AtemSocketState::Established {
|
||||
session_id: p.session_id(),
|
||||
};
|
||||
debug!("ATEM state dump done, session ID is {}", p.session_id());
|
||||
self.ack(p.session_id(), p.local_sequence_number()).await?;
|
||||
}
|
||||
}
|
||||
AtemSocketState::Established { session_id } => {
|
||||
if p.flags().contains(PacketFlag::AckRequest) {
|
||||
self.ack(session_id, p.local_sequence_number()).await?;
|
||||
}
|
||||
}
|
||||
AtemSocketState::Closed => unreachable!(),
|
||||
}
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// Send a packet to the ATEM. The ATEM will respond with an ACK - it's up to the caller to handle packet retransmission
|
||||
pub async fn send<T: AsRef<[u8]>>(&mut self, p: AtemPacket<T>) -> Result<(), AtemError<I>> {
|
||||
self.inner
|
||||
.send_to(p.inner().as_ref(), self.addr)
|
||||
.await
|
||||
.map_err(|e| AtemError::Send(e))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub const COMMAND_CONNECT_HELLO: [u8; 20] = [
|
||||
0x10, 0x14, 0x53, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
@@ -1,5 +1,3 @@
|
||||
pub mod atem_field;
|
||||
pub mod atem_packet;
|
||||
#[cfg(feature = "std")]
|
||||
mod atem_packet;
|
||||
pub mod atem_socket;
|
||||
pub mod atem_socket_two;
|
||||
pub mod atem_util;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{boxed::Box, collections::HashMap, fmt::Debug, string::String, sync::Arc, vec::Vec};
|
||||
use std::{collections::HashMap, fmt::Debug, sync::Arc};
|
||||
|
||||
use crate::{enums::ProtocolVersion, state::AtemState};
|
||||
|
||||
@@ -8,51 +8,18 @@ pub trait DeserializedCommand: Send + Sync + Debug {
|
||||
}
|
||||
|
||||
pub trait CommandDeserializer: Send + Sync {
|
||||
fn deserialize(&self, buffer: &[u8], version: &ProtocolVersion)
|
||||
-> Arc<dyn DeserializedCommand>;
|
||||
fn deserialize(&self, buffer: &[u8]) -> Arc<dyn DeserializedCommand>;
|
||||
}
|
||||
|
||||
pub trait SerializableCommand: Send + Sync {
|
||||
pub trait SerializableCommand {
|
||||
fn payload(&self, version: &ProtocolVersion) -> Vec<u8>;
|
||||
}
|
||||
|
||||
impl<C: SerializableCommand + ?Sized> SerializableCommand for Box<C> {
|
||||
fn payload(&self, version: &ProtocolVersion) -> Vec<u8> {
|
||||
(**self).payload(version)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: SerializableCommand + ?Sized> SerializableCommand for &'_ Box<C> {
|
||||
fn payload(&self, version: &ProtocolVersion) -> Vec<u8> {
|
||||
(**self).payload(version)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BasicWritableCommand: SerializableCommand + Send + Sync {
|
||||
pub trait BasicWritableCommand: SerializableCommand {
|
||||
fn get_raw_name(&self) -> &'static str;
|
||||
fn get_minimum_version(&self) -> ProtocolVersion;
|
||||
}
|
||||
|
||||
impl<C: BasicWritableCommand + ?Sized> BasicWritableCommand for Box<C> {
|
||||
fn get_raw_name(&self) -> &'static str {
|
||||
(**self).get_raw_name()
|
||||
}
|
||||
|
||||
fn get_minimum_version(&self) -> ProtocolVersion {
|
||||
(**self).get_minimum_version()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: BasicWritableCommand + ?Sized> BasicWritableCommand for &'_ Box<C> {
|
||||
fn get_raw_name(&self) -> &'static str {
|
||||
(**self).get_raw_name()
|
||||
}
|
||||
|
||||
fn get_minimum_version(&self) -> ProtocolVersion {
|
||||
(**self).get_minimum_version()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WritableCommand: BasicWritableCommand {
|
||||
fn get_mask_flag(&self) -> HashMap<String, f64>;
|
||||
fn get_flag(&self) -> f64;
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
pub mod audio_mixer_config;
|
||||
pub mod media_pool_config;
|
||||
pub mod mix_effect_block_config;
|
||||
pub mod multiviewer_config;
|
||||
pub mod product_identifier;
|
||||
pub mod topology;
|
||||
pub mod version;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::enums::ProtocolVersion;
|
||||
|
||||
use super::command_base::{CommandDeserializer, DeserializedCommand};
|
||||
|
||||
pub const DESERIALIZE_VERSION_RAW_NAME: &str = "_ver";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VersionCommand {
|
||||
pub version: ProtocolVersion,
|
||||
}
|
||||
|
||||
impl DeserializedCommand for VersionCommand {
|
||||
fn raw_name(&self) -> &'static str {
|
||||
DESERIALIZE_VERSION_RAW_NAME
|
||||
}
|
||||
|
||||
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
|
||||
state.info.api_version = self.version;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct VersionCommandDeserializer {}
|
||||
|
||||
impl CommandDeserializer for VersionCommandDeserializer {
|
||||
fn deserialize(&self, buffer: &[u8]) -> std::sync::Arc<dyn DeserializedCommand> {
|
||||
let version = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
|
||||
let version: ProtocolVersion = version.try_into().expect("Invalid protocol version");
|
||||
|
||||
Arc::new(VersionCommand { version })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
commands::command_base::{CommandDeserializer, DeserializedCommand},
|
||||
state::{audio::AtemClassicAudioState, info::AudioMixerInfo},
|
||||
};
|
||||
|
||||
pub const DESERIALIZE_AUDIO_MIXER_CONFIG_NAME: &str = "_AMC";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AudioMixerConfig {
|
||||
inputs: u8,
|
||||
monitors: u8,
|
||||
headphones: u8,
|
||||
}
|
||||
|
||||
impl DeserializedCommand for AudioMixerConfig {
|
||||
fn raw_name(&self) -> &'static str {
|
||||
DESERIALIZE_AUDIO_MIXER_CONFIG_NAME
|
||||
}
|
||||
|
||||
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
|
||||
state.info.audio_mixer = Some(AudioMixerInfo::new(
|
||||
self.inputs,
|
||||
self.monitors,
|
||||
self.headphones,
|
||||
));
|
||||
state.audio = Some(AtemClassicAudioState::new(self.inputs, self.monitors != 0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AudioMixerConfigDeserializer {}
|
||||
|
||||
impl CommandDeserializer for AudioMixerConfigDeserializer {
|
||||
fn deserialize(
|
||||
&self,
|
||||
buffer: &[u8],
|
||||
version: &crate::enums::ProtocolVersion,
|
||||
) -> std::sync::Arc<dyn DeserializedCommand> {
|
||||
Arc::new(AudioMixerConfig {
|
||||
inputs: buffer[0],
|
||||
monitors: buffer[1],
|
||||
headphones: buffer[2],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
commands::command_base::{CommandDeserializer, DeserializedCommand},
|
||||
state::info::MediaPoolInfo,
|
||||
};
|
||||
|
||||
pub const DESERIALIZE_MEDIA_POOL_CONFIG_NAME: &str = "_mpl";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MediaPoolConfig {
|
||||
still_count: u8,
|
||||
clip_count: u8,
|
||||
}
|
||||
|
||||
impl DeserializedCommand for MediaPoolConfig {
|
||||
fn raw_name(&self) -> &'static str {
|
||||
DESERIALIZE_MEDIA_POOL_CONFIG_NAME
|
||||
}
|
||||
|
||||
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
|
||||
state.info.media_pool = Some(MediaPoolInfo::new(self.still_count, self.clip_count))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MediaPoolConfigDeserializer {}
|
||||
|
||||
impl CommandDeserializer for MediaPoolConfigDeserializer {
|
||||
fn deserialize(
|
||||
&self,
|
||||
buffer: &[u8],
|
||||
_version: &crate::enums::ProtocolVersion,
|
||||
) -> std::sync::Arc<dyn DeserializedCommand> {
|
||||
Arc::new(MediaPoolConfig {
|
||||
still_count: buffer[0],
|
||||
clip_count: buffer[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
commands::command_base::{CommandDeserializer, DeserializedCommand},
|
||||
state::info::MixEffectInfo,
|
||||
};
|
||||
|
||||
pub const DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME: &str = "_MeC";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MixEffectBlockConfig {
|
||||
mix_effect: u8,
|
||||
key_count: u8,
|
||||
}
|
||||
|
||||
impl DeserializedCommand for MixEffectBlockConfig {
|
||||
fn raw_name(&self) -> &'static str {
|
||||
DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME
|
||||
}
|
||||
|
||||
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
|
||||
state.info.mix_effects[self.mix_effect as usize] = Some(MixEffectInfo::new(self.key_count));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MixEffectBlockConfigDeserializer {}
|
||||
|
||||
impl CommandDeserializer for MixEffectBlockConfigDeserializer {
|
||||
fn deserialize(
|
||||
&self,
|
||||
buffer: &[u8],
|
||||
_version: &crate::enums::ProtocolVersion,
|
||||
) -> std::sync::Arc<dyn DeserializedCommand> {
|
||||
Arc::new(MixEffectBlockConfig {
|
||||
mix_effect: buffer[0],
|
||||
key_count: buffer[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
commands::command_base::{CommandDeserializer, DeserializedCommand},
|
||||
enums::ProtocolVersion,
|
||||
state::info::MultiviewerInfo,
|
||||
};
|
||||
|
||||
pub const DESERIALIZE_MULTIVIEWER_NAME: &str = "_MvC";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MultiviewerConfig {
|
||||
count: Option<u8>,
|
||||
window_count: u8,
|
||||
}
|
||||
|
||||
impl DeserializedCommand for MultiviewerConfig {
|
||||
fn raw_name(&self) -> &'static str {
|
||||
DESERIALIZE_MULTIVIEWER_NAME
|
||||
}
|
||||
|
||||
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
|
||||
// TODO: This can't be right...
|
||||
|
||||
let existing_count = match &state.info.multiviewer {
|
||||
Some(multiviewer) => multiviewer.count().as_ref().copied(),
|
||||
None => None,
|
||||
};
|
||||
let count = match self.count {
|
||||
Some(count) => Some(count),
|
||||
None => existing_count,
|
||||
};
|
||||
state.info.multiviewer = Some(MultiviewerInfo::new(count, self.window_count));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MultiviewerConfigDeserializer {}
|
||||
|
||||
impl CommandDeserializer for MultiviewerConfigDeserializer {
|
||||
fn deserialize(
|
||||
&self,
|
||||
buffer: &[u8],
|
||||
version: &crate::enums::ProtocolVersion,
|
||||
) -> std::sync::Arc<dyn DeserializedCommand> {
|
||||
if *version >= ProtocolVersion::V8_1_1 {
|
||||
Arc::new(MultiviewerConfig {
|
||||
count: None,
|
||||
window_count: buffer[1],
|
||||
})
|
||||
} else {
|
||||
Arc::new(MultiviewerConfig {
|
||||
count: Some(buffer[0]),
|
||||
window_count: buffer[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use std::{
|
||||
ffi::CString,
|
||||
string::{String, ToString},
|
||||
sync::Arc,
|
||||
vec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
commands::command_base::{CommandDeserializer, DeserializedCommand},
|
||||
enums::{Model, ProtocolVersion},
|
||||
};
|
||||
|
||||
pub const DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME: &str = "_pin";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProductIdentifier {
|
||||
pub product_identifier: String,
|
||||
pub model: Model,
|
||||
}
|
||||
|
||||
impl DeserializedCommand for ProductIdentifier {
|
||||
fn raw_name(&self) -> &'static str {
|
||||
DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME
|
||||
}
|
||||
|
||||
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
|
||||
state.info.product_identifier = Some(self.product_identifier.clone());
|
||||
state.info.model = self.model.clone();
|
||||
|
||||
match state.info.model {
|
||||
Model::TwoME
|
||||
| Model::TwoME4K
|
||||
| Model::TwoMEBS4K
|
||||
| Model::Constellation
|
||||
| Model::Constellation8K
|
||||
| Model::ConstellationHD4ME
|
||||
| Model::Constellation4K4ME => {
|
||||
state.info.power = vec![false, false];
|
||||
}
|
||||
_ => {
|
||||
state.info.power = vec![false];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProductIdentifierDeserializer {}
|
||||
|
||||
impl CommandDeserializer for ProductIdentifierDeserializer {
|
||||
fn deserialize(
|
||||
&self,
|
||||
buffer: &[u8],
|
||||
version: &ProtocolVersion,
|
||||
) -> Arc<dyn DeserializedCommand> {
|
||||
let null_byte_index = buffer
|
||||
.iter()
|
||||
.position(|byte| *byte == b'\0')
|
||||
.expect("No null byte");
|
||||
let product_identifier =
|
||||
CString::from_vec_with_nul(buffer[..(null_byte_index + 1)].to_vec())
|
||||
.expect("Malformed string");
|
||||
let model = buffer[40];
|
||||
|
||||
Arc::new(ProductIdentifier {
|
||||
product_identifier: product_identifier
|
||||
.to_str()
|
||||
.expect("Invalid rust string")
|
||||
.to_string(),
|
||||
model: model.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
use std::{sync::Arc, vec};
|
||||
|
||||
use crate::{
|
||||
commands::command_base::{CommandDeserializer, DeserializedCommand},
|
||||
enums::ProtocolVersion,
|
||||
state::info::{AtemCapabilites, MultiviewerInfo},
|
||||
};
|
||||
|
||||
pub const DESERIALIZE_TOPOLOGY_RAW_NAME: &str = "_top";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Topology {
|
||||
mix_effects: u8,
|
||||
sources: u8,
|
||||
auxilliaries: u8,
|
||||
mix_minus_outputs: u8,
|
||||
media_players: u8,
|
||||
multiviewers: Option<u8>,
|
||||
serial_ports: u8,
|
||||
max_hyperdecks: u8,
|
||||
dves: u8,
|
||||
stingers: u8,
|
||||
super_sources: u8,
|
||||
talkback_channels: u8,
|
||||
downstream_keyers: u8,
|
||||
camera_control: bool,
|
||||
advanced_chroma_keyers: bool,
|
||||
only_configurable_outputs: bool,
|
||||
}
|
||||
|
||||
impl DeserializedCommand for Topology {
|
||||
fn raw_name(&self) -> &'static str {
|
||||
DESERIALIZE_TOPOLOGY_RAW_NAME
|
||||
}
|
||||
|
||||
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
|
||||
state.info.capabilities = Some(AtemCapabilites::new(
|
||||
self.mix_effects,
|
||||
self.sources,
|
||||
self.auxilliaries,
|
||||
self.mix_minus_outputs,
|
||||
self.media_players,
|
||||
self.serial_ports,
|
||||
self.max_hyperdecks,
|
||||
self.dves,
|
||||
self.stingers,
|
||||
self.super_sources,
|
||||
self.talkback_channels,
|
||||
self.downstream_keyers,
|
||||
self.camera_control,
|
||||
self.advanced_chroma_keyers,
|
||||
self.only_configurable_outputs,
|
||||
));
|
||||
|
||||
let window_count = if let Some(mv) = &state.info.multiviewer {
|
||||
*mv.window_count()
|
||||
} else {
|
||||
10
|
||||
};
|
||||
|
||||
state.info.multiviewer = Some(MultiviewerInfo::new(self.multiviewers, window_count));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TopologyDeserializer {}
|
||||
|
||||
impl CommandDeserializer for TopologyDeserializer {
|
||||
fn deserialize(
|
||||
&self,
|
||||
buffer: &[u8],
|
||||
version: &ProtocolVersion,
|
||||
) -> Arc<dyn DeserializedCommand> {
|
||||
let v230offset = if *version > ProtocolVersion::V8_0_1 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let multiviewers = if v230offset > 0 {
|
||||
Some(buffer[6])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let advanced_chroma_keyers = if buffer.len() > 20 {
|
||||
buffer[21 + v230offset] == 1
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let only_configurable_outputs = if buffer.len() > 20 {
|
||||
buffer[22 + v230offset] == 1
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Arc::new(Topology {
|
||||
mix_effects: buffer[0],
|
||||
sources: buffer[1],
|
||||
downstream_keyers: buffer[2],
|
||||
auxilliaries: buffer[3],
|
||||
mix_minus_outputs: buffer[4],
|
||||
media_players: buffer[5],
|
||||
multiviewers,
|
||||
serial_ports: buffer[6 + v230offset],
|
||||
max_hyperdecks: buffer[7 + v230offset],
|
||||
dves: buffer[8 + v230offset],
|
||||
stingers: buffer[9 + v230offset],
|
||||
super_sources: buffer[10 + v230offset],
|
||||
talkback_channels: buffer[12 + v230offset],
|
||||
camera_control: buffer[17 + v230offset] == 1,
|
||||
advanced_chroma_keyers,
|
||||
only_configurable_outputs,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
use crate::{commands::command_base::DeserializedCommand, enums::ProtocolVersion};
|
||||
|
||||
pub const DESERIALIZE_VERSION_RAW_NAME: &str = "_ver";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Version {
|
||||
pub version: ProtocolVersion,
|
||||
}
|
||||
|
||||
impl DeserializedCommand for Version {
|
||||
fn raw_name(&self) -> &'static str {
|
||||
DESERIALIZE_VERSION_RAW_NAME
|
||||
}
|
||||
|
||||
fn apply_to_state(&self, state: &mut crate::state::AtemState) {
|
||||
state.info.api_version = self.version.clone();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_version(buffer: &[u8]) -> Version {
|
||||
let version = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
|
||||
let version: ProtocolVersion = version.try_into().expect("Invalid protocol version");
|
||||
|
||||
Version { version }
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::enums::ProtocolVersion;
|
||||
|
||||
use super::command_base::{CommandDeserializer, DeserializedCommand};
|
||||
|
||||
pub const DESERIALIZE_INIT_COMPLETE_RAW_NAME: &str = "InCm";
|
||||
@@ -21,11 +19,7 @@ impl DeserializedCommand for InitComplete {
|
||||
pub struct InitCompleteDeserializer {}
|
||||
|
||||
impl CommandDeserializer for InitCompleteDeserializer {
|
||||
fn deserialize(
|
||||
&self,
|
||||
_buffer: &[u8],
|
||||
version: &ProtocolVersion,
|
||||
) -> Arc<dyn DeserializedCommand> {
|
||||
fn deserialize(&self, _buffer: &[u8]) -> std::sync::Arc<dyn DeserializedCommand> {
|
||||
Arc::new(InitComplete {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use derive_new::new;
|
||||
use std::{sync::Arc, vec, vec::Vec};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
commands::command_base::{
|
||||
BasicWritableCommand, CommandDeserializer, DeserializedCommand, SerializableCommand,
|
||||
},
|
||||
enums::ProtocolVersion,
|
||||
state::util::get_mix_effect,
|
||||
};
|
||||
|
||||
@@ -60,11 +58,7 @@ impl DeserializedCommand for ProgramInput {
|
||||
pub struct ProgramInputDeserializer {}
|
||||
|
||||
impl CommandDeserializer for ProgramInputDeserializer {
|
||||
fn deserialize(
|
||||
&self,
|
||||
buffer: &[u8],
|
||||
version: &ProtocolVersion,
|
||||
) -> Arc<dyn DeserializedCommand> {
|
||||
fn deserialize(&self, buffer: &[u8]) -> Arc<dyn DeserializedCommand> {
|
||||
let mix_effect = buffer[0];
|
||||
let source = u16::from_be_bytes([buffer[2], buffer[3]]);
|
||||
|
||||
|
||||
@@ -1,53 +1,37 @@
|
||||
use std::{boxed::Box, collections::VecDeque, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
atem_lib::atem_packet::RawFields,
|
||||
commands::device_profile::version::{DESERIALIZE_VERSION_RAW_NAME, deserialize_version},
|
||||
enums::ProtocolVersion,
|
||||
};
|
||||
use std::{collections::VecDeque, sync::Arc};
|
||||
|
||||
use super::{
|
||||
command_base::{CommandDeserializer, DeserializedCommand},
|
||||
device_profile::{
|
||||
audio_mixer_config::{AudioMixerConfigDeserializer, DESERIALIZE_AUDIO_MIXER_CONFIG_NAME},
|
||||
media_pool_config::{DESERIALIZE_MEDIA_POOL_CONFIG_NAME, MediaPoolConfigDeserializer},
|
||||
mix_effect_block_config::{
|
||||
DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME, MixEffectBlockConfigDeserializer,
|
||||
},
|
||||
multiviewer_config::{DESERIALIZE_MULTIVIEWER_NAME, MultiviewerConfigDeserializer},
|
||||
product_identifier::{
|
||||
DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME, ProductIdentifierDeserializer,
|
||||
},
|
||||
topology::{DESERIALIZE_TOPOLOGY_RAW_NAME, TopologyDeserializer},
|
||||
},
|
||||
init_complete::{DESERIALIZE_INIT_COMPLETE_RAW_NAME, InitCompleteDeserializer},
|
||||
mix_effects::program_input::{DESERIALIZE_PROGRAM_INPUT_RAW_NAME, ProgramInputDeserializer},
|
||||
tally_by_source::{DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME, TallyBySourceDeserializer},
|
||||
time::{DESERIALIZE_TIME_RAW_NAME, TimeDeserializer},
|
||||
device_profile::{VersionCommandDeserializer, DESERIALIZE_VERSION_RAW_NAME},
|
||||
init_complete::{InitCompleteDeserializer, DESERIALIZE_INIT_COMPLETE_RAW_NAME},
|
||||
mix_effects::program_input::{ProgramInputDeserializer, DESERIALIZE_PROGRAM_INPUT_RAW_NAME},
|
||||
tally_by_source::{TallyBySourceDeserializer, DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME},
|
||||
time::{TimeDeserializer, DESERIALIZE_TIME_RAW_NAME},
|
||||
};
|
||||
|
||||
pub fn deserialize_commands<T: AsRef<[u8]>>(
|
||||
payload: T,
|
||||
version: &mut ProtocolVersion,
|
||||
) -> VecDeque<Arc<dyn DeserializedCommand>> {
|
||||
let mut parsed_commands: VecDeque<Arc<dyn DeserializedCommand>> = VecDeque::new();
|
||||
pub fn deserialize_commands(payload: &[u8]) -> VecDeque<Arc<dyn DeserializedCommand>> {
|
||||
let mut parsed_commands = VecDeque::new();
|
||||
let mut head = 0;
|
||||
|
||||
for field in RawFields::new(payload.as_ref()) {
|
||||
let name: &str = field.r#type.try_into().unwrap();
|
||||
log::debug!("Received command {} with length {}", name, field.data.len(),);
|
||||
while payload.len() > head + 8 {
|
||||
let length = u16::from_be_bytes([payload[head], payload[head + 1]]) as usize;
|
||||
let Ok(name) = String::from_utf8(payload[(head + 4)..(head + 8)].to_vec()) else {
|
||||
break;
|
||||
};
|
||||
|
||||
if field.r#type == DESERIALIZE_VERSION_RAW_NAME {
|
||||
let version_command = deserialize_version(field.data);
|
||||
*version = version_command.version.clone();
|
||||
log::info!("Switched to protocol version {}", version);
|
||||
parsed_commands.push_back(Arc::new(version_command));
|
||||
} else if let Some(deserializer) = command_deserializer_from_string(name) {
|
||||
let deserialized_command = deserializer.deserialize(field.data, version);
|
||||
if length < 8 {
|
||||
break;
|
||||
}
|
||||
|
||||
log::debug!("Received command {} with length {}", name, length);
|
||||
|
||||
if let Some(deserializer) = command_deserializer_from_string(name.as_str()) {
|
||||
let deserialized_command = deserializer.deserialize(&payload[head + 8..head + length]);
|
||||
log::debug!("Received {:?}", deserialized_command);
|
||||
parsed_commands.push_back(deserialized_command);
|
||||
} else {
|
||||
log::warn!("Received command {name} for which there is no deserializer.");
|
||||
}
|
||||
|
||||
head += length;
|
||||
}
|
||||
|
||||
parsed_commands
|
||||
@@ -55,20 +39,11 @@ pub fn deserialize_commands<T: AsRef<[u8]>>(
|
||||
|
||||
fn command_deserializer_from_string(command_str: &str) -> Option<Box<dyn CommandDeserializer>> {
|
||||
match command_str {
|
||||
DESERIALIZE_VERSION_RAW_NAME => Some(Box::<VersionCommandDeserializer>::default()),
|
||||
DESERIALIZE_INIT_COMPLETE_RAW_NAME => Some(Box::<InitCompleteDeserializer>::default()),
|
||||
DESERIALIZE_PROGRAM_INPUT_RAW_NAME => Some(Box::<ProgramInputDeserializer>::default()),
|
||||
DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME => Some(Box::<TallyBySourceDeserializer>::default()),
|
||||
DESERIALIZE_TIME_RAW_NAME => Some(Box::<TimeDeserializer>::default()),
|
||||
DESERIALIZE_TOPOLOGY_RAW_NAME => Some(Box::<TopologyDeserializer>::default()),
|
||||
DESERIALIZE_MIX_EFFECT_BLOCK_CONFIG_NAME => {
|
||||
Some(Box::<MixEffectBlockConfigDeserializer>::default())
|
||||
}
|
||||
DESERIALIZE_PRODUCT_IDENTIFIER_RAW_NAME => {
|
||||
Some(Box::<ProductIdentifierDeserializer>::default())
|
||||
}
|
||||
DESERIALIZE_MEDIA_POOL_CONFIG_NAME => Some(Box::<MediaPoolConfigDeserializer>::default()),
|
||||
DESERIALIZE_MULTIVIEWER_NAME => Some(Box::<MultiviewerConfigDeserializer>::default()),
|
||||
DESERIALIZE_AUDIO_MIXER_CONFIG_NAME => Some(Box::<AudioMixerConfigDeserializer>::default()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use derive_new::new;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::enums::ProtocolVersion;
|
||||
|
||||
use super::command_base::{CommandDeserializer, DeserializedCommand};
|
||||
|
||||
pub const DESERIALIZE_TALLY_BY_SOURCE_RAW_NAME: &str = "TlSr";
|
||||
@@ -32,11 +29,7 @@ impl DeserializedCommand for TallyBySource {
|
||||
pub struct TallyBySourceDeserializer {}
|
||||
|
||||
impl CommandDeserializer for TallyBySourceDeserializer {
|
||||
fn deserialize(
|
||||
&self,
|
||||
buffer: &[u8],
|
||||
version: &ProtocolVersion,
|
||||
) -> Arc<dyn DeserializedCommand> {
|
||||
fn deserialize(&self, buffer: &[u8]) -> Arc<dyn DeserializedCommand> {
|
||||
let source_count = u16::from_be_bytes([buffer[0], buffer[1]]) as usize;
|
||||
|
||||
log::debug!("{:?}", buffer);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::enums::ProtocolVersion;
|
||||
|
||||
use super::command_base::{CommandDeserializer, DeserializedCommand};
|
||||
|
||||
pub const DESERIALIZE_TIME_RAW_NAME: &str = "Time";
|
||||
@@ -35,8 +33,7 @@ impl CommandDeserializer for TimeDeserializer {
|
||||
fn deserialize(
|
||||
&self,
|
||||
buffer: &[u8],
|
||||
version: &ProtocolVersion,
|
||||
) -> Arc<dyn DeserializedCommand> {
|
||||
) -> std::sync::Arc<dyn super::command_base::DeserializedCommand> {
|
||||
let info = TimeInfo {
|
||||
hour: buffer[0],
|
||||
minute: buffer[1],
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
pub enum Model {
|
||||
#[default]
|
||||
Unknown = 0x00,
|
||||
@@ -21,59 +19,9 @@ pub enum Model {
|
||||
MiniProISO = 0x0f,
|
||||
MiniExtreme = 0x10,
|
||||
MiniExtremeISO = 0x11,
|
||||
ConstellationHD1ME = 0x12,
|
||||
ConstellationHD2ME = 0x13,
|
||||
ConstellationHD4ME = 0x14,
|
||||
SDI = 0x15,
|
||||
SDIProISO = 0x16,
|
||||
SDIExtremeISO = 0x17,
|
||||
// 0x18 ??
|
||||
// 0x19 ??
|
||||
TelevisionStudioHD8 = 0x1a,
|
||||
TelevisionStudioHD8ISO = 0x1b,
|
||||
// 0x1c ??
|
||||
// 0x1d ??
|
||||
Constellation4K4ME = 0x1e,
|
||||
// 0x1f ??
|
||||
TelevisionStudio4K8 = 0x20,
|
||||
}
|
||||
|
||||
impl From<u8> for Model {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
0x01 => Model::TVS,
|
||||
0x02 => Model::OneME,
|
||||
0x03 => Model::TwoME,
|
||||
0x04 => Model::PS4K,
|
||||
0x05 => Model::OneME4K,
|
||||
0x06 => Model::TwoME4K,
|
||||
0x07 => Model::TwoMEBS4K,
|
||||
0x08 => Model::TVSHD,
|
||||
0x09 => Model::TVSProHD,
|
||||
0x0a => Model::TVSPro4K,
|
||||
0x0b => Model::Constellation,
|
||||
0x0c => Model::Constellation8K,
|
||||
0x0d => Model::Mini,
|
||||
0x0e => Model::MiniPro,
|
||||
0x0f => Model::MiniProISO,
|
||||
0x10 => Model::MiniExtreme,
|
||||
0x11 => Model::MiniExtremeISO,
|
||||
0x12 => Model::ConstellationHD1ME,
|
||||
0x13 => Model::ConstellationHD2ME,
|
||||
0x14 => Model::ConstellationHD4ME,
|
||||
0x15 => Model::SDI,
|
||||
0x16 => Model::SDIProISO,
|
||||
0x17 => Model::SDIExtremeISO,
|
||||
0x1a => Model::TelevisionStudioHD8,
|
||||
0x1b => Model::TelevisionStudioHD8ISO,
|
||||
0x1e => Model::Constellation4K4ME,
|
||||
0x20 => Model::TelevisionStudio4K8,
|
||||
_ => Model::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub enum ProtocolVersion {
|
||||
#[default]
|
||||
Unknown = 0,
|
||||
@@ -100,19 +48,6 @@ impl TryFrom<u32> for ProtocolVersion {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ProtocolVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ProtocolVersion::Unknown => write!(f, "Unknown"),
|
||||
ProtocolVersion::V7_2 => write!(f, "v7.2"),
|
||||
ProtocolVersion::V7_5_2 => write!(f, "v7.5.2"),
|
||||
ProtocolVersion::V8_0 => write!(f, "v8.0"),
|
||||
ProtocolVersion::V8_0_1 => write!(f, "v8.0.1"),
|
||||
ProtocolVersion::V8_1_1 => write!(f, "v8.1.1"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum TransitionStyle {
|
||||
MIX = 0x00,
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
#![no_std]
|
||||
#[macro_use]
|
||||
extern crate derive_new;
|
||||
#[macro_use]
|
||||
extern crate derive_getters;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
extern crate std;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub mod atem;
|
||||
pub mod atem_lib;
|
||||
#[cfg(feature = "std")]
|
||||
pub mod commands;
|
||||
#[cfg(feature = "std")]
|
||||
pub mod enums;
|
||||
#[cfg(feature = "std")]
|
||||
pub mod state;
|
||||
#[cfg(feature = "std")]
|
||||
pub mod tally;
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
use std::{string::String, vec::Vec};
|
||||
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
#[derive(Clone, PartialEq, Getters, new, Default)]
|
||||
pub struct MacroPlayerState {
|
||||
pub is_running: bool,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::enums::{AudioMixOption, AudioSourceType, ExternalPortType};
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
|
||||
pub type AudioChannel = ClassicAudioChannel;
|
||||
pub type AudioMasterChannel = ClassicAudioMasterChannel;
|
||||
@@ -46,28 +44,14 @@ pub struct ClassicAudioHeadphoneOutputChannel {
|
||||
pub talkback_gain: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Getters)]
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct AtemClassicAudioState {
|
||||
number_of_channels: u8,
|
||||
has_monitor: bool,
|
||||
number_of_channels: Option<f64>,
|
||||
has_monitor: Option<bool>,
|
||||
pub channels: HashMap<u64, ClassicAudioChannel>,
|
||||
pub monitor: Option<ClassicAudioMonitorChannel>,
|
||||
pub headphones: Option<ClassicAudioHeadphoneOutputChannel>,
|
||||
pub master: Option<ClassicAudioMasterChannel>,
|
||||
|
||||
pub audio_follow_video_crossfade_transition_enabled: bool,
|
||||
}
|
||||
|
||||
impl AtemClassicAudioState {
|
||||
pub fn new(number_of_channels: u8, has_monitor: bool) -> Self {
|
||||
Self {
|
||||
number_of_channels,
|
||||
has_monitor,
|
||||
channels: Default::default(),
|
||||
monitor: Default::default(),
|
||||
headphones: Default::default(),
|
||||
master: Default::default(),
|
||||
audio_follow_video_crossfade_transition_enabled: false,
|
||||
}
|
||||
}
|
||||
pub audio_follow_video_crossfade_transition_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct ColorGeneratorState {
|
||||
pub hue: u64,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct Timecode {
|
||||
pub hours: u64,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
use std::{collections::HashMap, string::String, vec::Vec};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::enums::{
|
||||
ExternalPortType, FairlightAnalogInputLevel, FairlightAudioMixOption, FairlightAudioSourceType,
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
use std::{string::String, vec::Vec};
|
||||
|
||||
use crate::enums::{Model, ProtocolVersion};
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct AtemCapabilites {
|
||||
mix_effects: u8,
|
||||
sources: u8,
|
||||
auxilliaries: u8,
|
||||
mix_minus_outputs: u8,
|
||||
media_players: u8,
|
||||
serial_ports: u8,
|
||||
max_hyperdecks: u8,
|
||||
dves: u8,
|
||||
stingers: u8,
|
||||
super_sources: u8,
|
||||
talkback_channels: u8,
|
||||
downstream_keyers: u8,
|
||||
sources: u64,
|
||||
auxilliaries: u64,
|
||||
mix_minus_outputs: u64,
|
||||
media_players: u64,
|
||||
serial_ports: u64,
|
||||
max_hyperdecks: u64,
|
||||
dves: u64,
|
||||
stingers: u64,
|
||||
super_sources: u64,
|
||||
talkback_channels: u64,
|
||||
downstream_keyers: u64,
|
||||
camera_control: bool,
|
||||
advanced_chroma_keyers: bool,
|
||||
only_configurable_outputs: bool,
|
||||
@@ -25,7 +21,7 @@ pub struct AtemCapabilites {
|
||||
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct MixEffectInfo {
|
||||
key_count: u8,
|
||||
key_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
@@ -35,9 +31,9 @@ pub struct SuperSourceInfo {
|
||||
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct AudioMixerInfo {
|
||||
inputs: u8,
|
||||
monitors: u8,
|
||||
headphones: u8,
|
||||
inputs: u64,
|
||||
monitors: u64,
|
||||
headphones: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
@@ -53,14 +49,14 @@ pub struct MacroPoolInfo {
|
||||
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct MediaPoolInfo {
|
||||
still_count: u8,
|
||||
clip_count: u8,
|
||||
still_count: u64,
|
||||
clip_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct MultiviewerInfo {
|
||||
count: Option<u8>,
|
||||
window_count: u8,
|
||||
count: u64,
|
||||
window_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, new)]
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use crate::enums::{ExternalPortType, InternalPortType, MeAvailability, SourceAvailability};
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
use std::string::String;
|
||||
use std::vec::Vec;
|
||||
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct InputChannel {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use crate::enums;
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
use std::string::String;
|
||||
use std::vec::Vec;
|
||||
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct MediaPlayer {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
use std::collections::HashMap;
|
||||
use std::string::String;
|
||||
|
||||
use crate::enums::{RecordingDiskStatus, RecordingError, RecordingStatus};
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::enums::{MultiViewerLayout, VideoMode};
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
use std::vec::Vec;
|
||||
|
||||
pub trait MultiViewerSourceState {
|
||||
fn get_source(&self) -> u64;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::enums::{StreamingError, StreamingStatus};
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
use std::string::String;
|
||||
|
||||
use super::common::Timecode;
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::enums::{TransitionSelection, TransitionStyle};
|
||||
use std::vec;
|
||||
|
||||
use super::{
|
||||
AtemState,
|
||||
settings::MultiViewer,
|
||||
video::{MixEffect, TransitionPosition, TransitionProperties, TransitionSettings},
|
||||
AtemState,
|
||||
};
|
||||
|
||||
pub fn create() -> AtemState {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
pub trait DownstreamKeyerBase {
|
||||
fn get_in_transition(&self) -> bool;
|
||||
fn get_remaining_frames(&self) -> f64;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::enums;
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
use std::vec::Vec;
|
||||
|
||||
mod downstream_keyers;
|
||||
mod super_source;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::enums;
|
||||
use derive_getters::Getters;
|
||||
use derive_new::new;
|
||||
|
||||
#[derive(Clone, PartialEq, Getters, new)]
|
||||
pub struct SuperSourceBox {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::string::String;
|
||||
#[derive(Debug)]
|
||||
pub struct TallyEvent {
|
||||
tally_state: TallyState,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[package]
|
||||
name = "atem-test"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
default-run = "main"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
atem-connection-rs = { path = "../atem-connection-rs" }
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
use std::{
|
||||
net::{Ipv4Addr, SocketAddrV4},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use atem_connection_rs::{
|
||||
atem::Atem,
|
||||
atem_lib::atem_socket::{AtemSocket, AtemSocketMessage},
|
||||
commands::mix_effects::program_input::ProgramInput,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Report;
|
||||
use tokio::{select, time::sleep};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// ATEM Rust Library Test App
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// IP of the ATEM to connect to
|
||||
#[arg(short, long)]
|
||||
ip: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
setup_logging().unwrap();
|
||||
|
||||
let (socket_message_tx, socket_message_rx) =
|
||||
tokio::sync::mpsc::channel::<AtemSocketMessage>(10);
|
||||
let (atem_event_tx, atem_event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let cancel = CancellationToken::new();
|
||||
|
||||
let atem_socket = AtemSocket::new(socket_message_rx, atem_event_tx);
|
||||
|
||||
let atem = Arc::new(Atem::new(atem_socket, socket_message_tx));
|
||||
let atem_thread = atem.clone();
|
||||
let atem_run = atem_thread.run(atem_event_rx, cancel);
|
||||
|
||||
let switch_loop = tokio::spawn(async move {
|
||||
let address = Ipv4Addr::from_str(&args.ip).unwrap();
|
||||
let socket = SocketAddrV4::new(address, 9910);
|
||||
atem.connect(socket.into()).await;
|
||||
|
||||
loop {
|
||||
sleep(Duration::from_millis(5000)).await;
|
||||
log::info!("Switch to source 1");
|
||||
atem.send_commands(vec![Box::new(ProgramInput::new(0, 1))])
|
||||
.await;
|
||||
log::info!("Switched to source 1");
|
||||
sleep(Duration::from_millis(5000)).await;
|
||||
log::info!("Switch to source 2");
|
||||
atem.send_commands(vec![Box::new(ProgramInput::new(0, 2))])
|
||||
.await;
|
||||
log::info!("Switched to source 2");
|
||||
}
|
||||
});
|
||||
|
||||
select! {
|
||||
_ = atem_run => {},
|
||||
_ = switch_loop => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_logging() -> Result<(), Report> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
}
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LOG", "debug");
|
||||
}
|
||||
}
|
||||
env_logger::init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
use atem_connection_rs::atem_lib::{atem_field::PrgI, atem_socket_two::tokio::TokioBackend};
|
||||
use clap::Parser;
|
||||
use color_eyre::Report;
|
||||
|
||||
/// ATEM Rust Library Test App
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// IP of the ATEM to connect to
|
||||
#[arg(short, long)]
|
||||
ip: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
setup_logging().unwrap();
|
||||
|
||||
let mut s = TokioBackend::new(args.ip.parse().unwrap()).await.unwrap();
|
||||
s.connect().await.expect("aa");
|
||||
let mut buf = [0u8; 2048];
|
||||
loop {
|
||||
let p = s.recv(&mut buf).await.unwrap();
|
||||
let prg: Vec<PrgI> = p.fields::<PrgI>().collect::<Result<_, _>>().unwrap();
|
||||
if !prg.is_empty() {
|
||||
println!("{:?}", prg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_logging() -> Result<(), Report> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
}
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LOG", "debug");
|
||||
}
|
||||
}
|
||||
env_logger::init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use std::{
|
||||
net::{Ipv4Addr, SocketAddrV4},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use atem_connection_rs::{
|
||||
atem::Atem,
|
||||
atem_lib::atem_socket::{AtemSocket, AtemSocketMessage},
|
||||
commands::mix_effects::program_input::ProgramInput,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Report;
|
||||
use tokio::time::sleep;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// ATEM Rust Library Test App
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// IP of the ATEM to connect to
|
||||
#[arg(short, long)]
|
||||
ip: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
setup_logging().unwrap();
|
||||
|
||||
let (socket_message_tx, socket_message_rx) =
|
||||
tokio::sync::mpsc::channel::<AtemSocketMessage>(10);
|
||||
let (atem_event_tx, atem_event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_task = cancel.clone();
|
||||
|
||||
let mut atem_socket = AtemSocket::new(atem_event_tx);
|
||||
tokio::spawn(async move {
|
||||
atem_socket.run(socket_message_rx, cancel_task).await;
|
||||
});
|
||||
|
||||
let atem = Arc::new(Atem::new(socket_message_tx));
|
||||
let atem_thread = atem.clone();
|
||||
tokio::spawn(async move {
|
||||
atem_thread.run(atem_event_rx, cancel).await;
|
||||
});
|
||||
|
||||
let address = Ipv4Addr::from_str(&args.ip).unwrap();
|
||||
let socket = SocketAddrV4::new(address, 9910);
|
||||
atem.connect(socket.into()).await;
|
||||
|
||||
loop {
|
||||
sleep(Duration::from_millis(5000)).await;
|
||||
log::info!("Switch to source 1");
|
||||
atem.send_commands(vec![Box::new(ProgramInput::new(0, 1))])
|
||||
.await;
|
||||
sleep(Duration::from_millis(5000)).await;
|
||||
log::info!("Switch to source 2");
|
||||
atem.send_commands(vec![Box::new(ProgramInput::new(0, 2))])
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_logging() -> Result<(), Report> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "debug");
|
||||
}
|
||||
env_logger::init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Generated
+126
-24
@@ -1,15 +1,70 @@
|
||||
{
|
||||
"nodes": {
|
||||
"naersk": {
|
||||
"devshell": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1745925850,
|
||||
"narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=",
|
||||
"lastModified": 1705332421,
|
||||
"narHash": "sha256-USpGLPme1IuqG78JNqSaRabilwkCyHmVWY0M9vYyqEA=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "83cb93d6d063ad290beee669f4badf9914cc16ec",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1698420672,
|
||||
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f",
|
||||
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -20,11 +75,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1749619289,
|
||||
"narHash": "sha256-qX6gXVjaCXXbcn6A9eSLUf8Fm07MgPGe5ir3++y2O1Q=",
|
||||
"lastModified": 1704161960,
|
||||
"narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f72be405a10668b8b00937b452f2145244103ebc",
|
||||
"rev": "63143ac2c9186be6d9da6035fa22620018c85932",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -36,11 +91,12 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1733412085,
|
||||
"narHash": "sha256-FillH0qdWDt/nlO6ED7h4cmN+G9uXwGjwmCnHs0QVYM=",
|
||||
"path": "/nix/store/5wvrlpfrwlmg24ywkybvidyzcvki6bwx-source",
|
||||
"rev": "4dc2fc4e62dbf62b84132fe526356fbac7b03541",
|
||||
"type": "path"
|
||||
"lastModified": 1705883077,
|
||||
"narHash": "sha256-ByzHHX3KxpU1+V0erFy8jpujTufimh6KaS/Iv3AciHk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5f5210aa20e343b7e35f40c033000db0ef80d7b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
@@ -49,11 +105,25 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1744536153,
|
||||
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||
"lastModified": 1705883077,
|
||||
"narHash": "sha256-ByzHHX3KxpU1+V0erFy8jpujTufimh6KaS/Iv3AciHk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||
"rev": "5f5210aa20e343b7e35f40c033000db0ef80d7b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1681358109,
|
||||
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -65,22 +135,24 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs": "nixpkgs_3",
|
||||
"rust-overlay": "rust-overlay",
|
||||
"utils": "utils"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1749695868,
|
||||
"narHash": "sha256-debjTLOyqqsYOUuUGQsAHskFXH5+Kx2t3dOo/FCoNRA=",
|
||||
"lastModified": 1705976279,
|
||||
"narHash": "sha256-Zx97bJ3+O8IP70uJPD//rRsr8bcxICISMTZUT/L9eFk=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "55f914d5228b5c8120e9e0f9698ed5b7214d09cd",
|
||||
"rev": "f889dc31ef97835834bdc3662394ebdb3c96b974",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -104,16 +176,46 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
inputs = {
|
||||
utils.url = "github:numtide/flake-utils";
|
||||
devshell.url = "github:numtide/devshell";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
};
|
||||
@@ -12,6 +13,7 @@
|
||||
nixpkgs,
|
||||
utils,
|
||||
naersk,
|
||||
devshell,
|
||||
rust-overlay,
|
||||
}:
|
||||
utils.lib.eachDefaultSystem (system: let
|
||||
@@ -33,10 +35,18 @@
|
||||
|
||||
apps.default = utils.lib.mkApp {drv = packages.default;};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [(rust.override {extensions = ["rust-src"];}) rust-analyzer gcc];
|
||||
|
||||
# Provide a dev env with rust and rust-analyzer
|
||||
devShells.default = let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [devshell.overlays.default];
|
||||
};
|
||||
formatter = pkgs.nixfmt-rfc-style;
|
||||
in
|
||||
pkgs.devshell.mkShell {
|
||||
motd = "Hello you wonderful person, I hope you are having a lovely day 💜";
|
||||
|
||||
packages = with pkgs; [(rust.override {extensions = ["rust-src"];}) rust-analyzer gcc];
|
||||
};
|
||||
formatter = pkgs.alejandra;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user