diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4e646a4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "spec/OECUASpecs"] + path = spec/OECUASpecs + url = https://github.com/ClassicMiniDIY/OECUASpecs.git diff --git a/Cargo.lock b/Cargo.lock index b7e8dbb..5d51d80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,181 +151,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" -[[package]] -name = "ashpd" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" -dependencies = [ - "async-fs", - "async-net", - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.2", - "raw-window-handle", - "serde", - "serde_repr", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus", -] - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.3", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix 1.1.3", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.3", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -407,19 +232,6 @@ dependencies = [ "objc2 0.6.3", ] -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "bstr" version = "1.12.1" @@ -547,9 +359,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.51" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", "jobserver", @@ -727,9 +539,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.4.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] @@ -1092,12 +904,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - [[package]] name = "enum-map" version = "2.7.3" @@ -1118,27 +924,6 @@ dependencies = [ "syn", ] -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[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", -] - [[package]] name = "enumn" version = "0.1.14" @@ -1197,33 +982,6 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "fax" version = "0.2.6" @@ -1267,15 +1025,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", @@ -1329,73 +1087,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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-io", - "futures-macro", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1418,9 +1109,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1593,12 +1284,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "hexf-parse" version = "0.2.1" @@ -1773,9 +1458,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", @@ -1880,9 +1565,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libloading" @@ -2127,19 +1812,6 @@ dependencies = [ "jni-sys", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nohash-hasher" version = "0.2.0" @@ -2507,23 +2179,14 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "247ad146e19b9437f8604c21f8652423595cf710ad108af40e77d3ae6e96b827" +checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" dependencies = [ + "libc", "libredox", ] -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "owned_ttf_parser" version = "0.19.0" @@ -2542,12 +2205,6 @@ dependencies = [ "ttf-parser 0.25.1", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -2610,7 +2267,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand", ] [[package]] @@ -2663,23 +2320,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -2758,15 +2398,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "printpdf" version = "0.7.0" @@ -2790,9 +2421,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -2820,18 +2451,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -2848,27 +2479,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", + "rand_core", ] [[package]] @@ -2877,15 +2488,6 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2945,7 +2547,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] @@ -2987,26 +2589,29 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "rfd" -version = "0.16.0" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" dependencies = [ - "ashpd", "block2 0.6.2", "dispatch2", "js-sys", + "libc", "log", "objc2 0.6.3", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", + "percent-encoding", "pollster", "raw-window-handle", - "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", "web-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3017,7 +2622,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3130,9 +2735,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "log", "once_cell", @@ -3234,9 +2839,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -3245,17 +2850,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_spanned" version = "0.6.9" @@ -3313,16 +2907,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - [[package]] name = "simd-adler32" version = "0.3.8" @@ -3469,9 +3053,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -3500,19 +3084,6 @@ dependencies = [ "xattr", ] -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -3631,9 +3202,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.10+spec-1.1.0" +version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ "indexmap", "serde_core", @@ -3805,20 +3376,9 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset", - "tempfile", - "winapi", -] - [[package]] name = "ultralog" -version = "2.1.2" +version = "2.1.4" dependencies = [ "anyhow", "dirs", @@ -3840,6 +3400,7 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_yaml", "strum", "tar", "thiserror 2.0.17", @@ -3853,9 +3414,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" @@ -3921,9 +3482,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -3931,12 +3492,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf-8" version = "0.7.6" @@ -3957,7 +3512,6 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde_core", "wasm-bindgen", ] @@ -4058,9 +3612,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" dependencies = [ "cc", "downcast-rs", @@ -4072,9 +3626,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ "bitflags 2.10.0", "rustix 1.1.3", @@ -4095,9 +3649,9 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.11" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" +checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" dependencies = [ "rustix 1.1.3", "wayland-client", @@ -4106,9 +3660,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.9" +version = "0.32.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" dependencies = [ "bitflags 2.10.0", "wayland-backend", @@ -4131,9 +3685,9 @@ dependencies = [ [[package]] name = "wayland-protocols-misc" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfe33d551eb8bffd03ff067a8b44bb963919157841a99957151299a6307d19c" +checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" dependencies = [ "bitflags 2.10.0", "wayland-backend", @@ -4144,9 +3698,9 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" +checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" dependencies = [ "bitflags 2.10.0", "wayland-backend", @@ -4157,9 +3711,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" dependencies = [ "bitflags 2.10.0", "wayland-backend", @@ -4170,9 +3724,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" dependencies = [ "proc-macro2", "quick-xml", @@ -4181,9 +3735,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" dependencies = [ "dlib", "log", @@ -4229,9 +3783,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] @@ -4338,22 +3892,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.11" @@ -4363,12 +3901,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-link" version = "0.2.1" @@ -4734,11 +4266,11 @@ dependencies = [ [[package]] name = "winresource" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b021990998587d4438bb672b5c5f034cbc927f51b45e3807ab7323645ef4899" +checksum = "17cdfa8da4b111045a5e47c7c839e6c5e11c942de1309bc624393ed5d87f89c6" dependencies = [ - "toml 0.9.10+spec-1.1.0", + "toml 0.9.11+spec-1.1.0", "version_check", ] @@ -4859,81 +4391,20 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zbus" -version = "5.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "nix", - "ordered-stream", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" -dependencies = [ - "serde", - "static_assertions", - "winnow", - "zvariant", -] - [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", @@ -4972,9 +4443,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", @@ -5046,9 +4517,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.2" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" +checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec" [[package]] name = "zopfli" @@ -5104,44 +4575,3 @@ checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] - -[[package]] -name = "zvariant" -version = "5.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" -dependencies = [ - "endi", - "enumflags2", - "serde", - "url", - "winnow", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn", - "winnow", -] diff --git a/Cargo.toml b/Cargo.toml index 4f050c7..b2dfc2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ultralog" -version = "2.1.2" +version = "2.1.4" edition = "2021" description = "A high-performance ECU log viewer written in Rust" authors = ["Cole Gentry"] @@ -21,19 +21,20 @@ path = "src/bin/test_parser.rs" [dependencies] # GUI Framework -eframe = { version = "0.33", default-features = false, features = [ +eframe = { version = "0.33.3", default-features = false, features = [ "default_fonts", "glow", "persistence", "x11", "wayland", ] } -egui_plot = "0.34" -egui_extras = { version = "0.33", features = ["image"] } +egui_plot = "0.34.0" +egui_extras = { version = "0.33.3", features = ["image"] } # Serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_yaml = "0.9" # For OpenECU Alliance adapter specs # Parsing regex = "1.12" @@ -47,14 +48,14 @@ meval = "0.2" dirs = "5.0" # File handling -rfd = "0.16" # Native file dialogs +rfd = "0.17" # Native file dialogs open = "5" # Open URLs in default browser memmap2 = "0.9" # Memory-mapped file loading for large files # Auto-update -ureq = { version = "3.0", features = ["json"] } # Minimal HTTP client +ureq = { version = "3.1", features = ["json"] } # Minimal HTTP client semver = "1.0" # Version comparison -zip = "2.2" # ZIP extraction for Windows updates +zip = "2.2" # ZIP extraction for Windows updates (v7.0 has breaking changes) flate2 = "1.0" # Gzip decompression for Linux updates tar = "0.4" # Tar archive extraction for Linux updates @@ -62,14 +63,14 @@ tar = "0.4" # Tar archive extraction for Linux updates image = { version = "0.25", default-features = false, features = ["png"] } # PDF generation for chart export -printpdf = "0.7" +printpdf = "0.7" # v0.8 has breaking API changes requiring code refactoring # Error handling -thiserror = "2.0" +thiserror = "2.0.17" anyhow = "1.0" # Internationalization -rust-i18n = "3" +rust-i18n = "3.1" # Logging tracing = "0.1" @@ -84,8 +85,8 @@ winresource = "0.1" # macOS-specific: set app name in dock [target.'cfg(target_os = "macos")'.dependencies] -objc2 = "0.6" -objc2-foundation = "0.3" +objc2 = "0.6.3" +objc2-foundation = "0.3.2" [dev-dependencies] # Note: cargo-tarpaulin is installed as a cargo subcommand, not a library dependency diff --git a/README.md b/README.md index 6b689b9..753f124 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A high-performance, cross-platform ECU log viewer written in Rust. ![CI](https://github.com/SomethingNew71/UltraLog/actions/workflows/ci.yml/badge.svg) ![License](https://img.shields.io/badge/license-AGPL--3.0-blue.svg) -![Version](https://img.shields.io/badge/version-2.1.2-green.svg) +![Version](https://img.shields.io/badge/version-2.1.4-green.svg) --- diff --git a/ULTRALOG_INTEGRATION.md b/ULTRALOG_INTEGRATION.md new file mode 100644 index 0000000..409eac9 --- /dev/null +++ b/ULTRALOG_INTEGRATION.md @@ -0,0 +1,433 @@ +# UltraLog Integration with OpenECU Alliance Specs + +This document describes how UltraLog integrates with OpenECU Alliance adapter specifications for channel normalization and metadata. + +## Implementation Status + +**Adapters (Log File Parsing):** + +- [x] Adapter specs embedded at compile time via `include_str!` +- [x] Spec-driven channel normalization from `source_names` +- [x] Integration with existing `normalize.rs` (spec as fallback) +- [x] Channel metadata lookup (min/max/precision/category) +- [x] 8 adapter specs integrated (Haltech, ECUMaster, Link, AiM, RomRaider, Speeduino, rusEFI, Emerald) +- [ ] Runtime adapter loading from user directory +- [ ] Generic CSV/binary parser driven by specs +- [ ] Adapter marketplace integration + +**Protocols (CAN Bus Real-Time Streaming):** + +- [x] Protocol specs embedded at compile time via `include_str!` +- [x] Protocol type definitions (ProtocolSpec, MessageSpec, SignalSpec) +- [x] 9 protocol specs integrated (Haltech, ECUMaster, Speeduino, rusEFI, AEM, Megasquirt, MaxxECU, Syvecs, Emtron) +- [x] Protocol registry API (get_protocols, get_protocol_by_id, find_protocols_by_vendor) +- [ ] CAN bus message encoder/decoder +- [ ] Real-time CAN streaming support +- [ ] DBC file export from protocol specs + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ UltraLog App │ +├─────────────────────────────────────────────────────────────────┤ +│ src/parsers/ src/adapters/ │ +│ ┌─────────────────────┐ ┌─────────────────────────┐ │ +│ │ Format-Specific │ │ Adapter Registry │ │ +│ │ Parsers (existing) │ │ (embedded YAML specs) │ │ +│ │ - haltech.rs │ │ - haltech-nsp │ │ +│ │ - ecumaster.rs │ ◄─────► │ - ecumaster-emu-csv │ │ +│ │ - link.rs │ uses │ - link-llg │ │ +│ │ - aim.rs │ for │ - aim-xrk │ │ +│ │ - romraider.rs │ metadata│ - romraider-csv │ │ +│ │ - speeduino.rs │ │ - speeduino-mlg │ │ +│ │ - emerald.rs │ │ - rusefi-mlg │ │ +│ └─────────────────────┘ │ - emerald-lg │ │ +│ └─────────────────────────┘ │ +├─────────────────────────────────────────────────────────────────┤ +│ src/normalize.rs │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ Normalization Priority: ││ +│ │ 1. Custom user mappings (highest) ││ +│ │ 2. Built-in hard-coded mappings ││ +│ │ 3. OpenECU Alliance spec source_names (fallback) ││ +│ └─────────────────────────────────────────────────────────────┘│ +├─────────────────────────────────────────────────────────────────┤ +│ src/parsers/types.rs - Channel │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ Channel metadata methods: ││ +│ │ - display_min() → parser value OR spec min ││ +│ │ - display_max() → parser value OR spec max ││ +│ │ - precision() → spec precision (decimal places) ││ +│ │ - category() → spec category (Engine, Fuel, etc.) ││ +│ │ - spec_metadata() → full ChannelMetadata struct ││ +│ └─────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Module Structure + +``` +src/ +├── adapters/ +│ ├── mod.rs # Module exports and re-exports +│ ├── types.rs # AdapterSpec, ProtocolSpec, ChannelSpec, MessageSpec, etc. +│ └── registry.rs # Spec loading, normalization maps, metadata lookup, protocol registry +├── normalize.rs # Field normalization (uses adapters for fallback) +└── parsers/ + └── types.rs # Channel type enhanced with spec metadata methods + +spec/OECUASpecs/ # Git submodule: github.com/ClassicMiniDIY/OECUASpecs +├── adapters/ # Log file format adapters (8 specs) +└── protocols/ # CAN bus protocol definitions (9 specs) +build.rs # Auto-downloads specs if submodule missing +``` + +## How It Works + +### 1. Spec Source: GitHub Submodule + +Adapter specs come from the [OECUASpecs](https://github.com/ClassicMiniDIY/OECUASpecs) repository as a git submodule: + +```bash +# Clone with submodules +git clone --recursive https://github.com/ClassicMiniDIY/UltraLog.git + +# Or initialize after cloning +git submodule update --init +``` + +The `build.rs` script ensures specs are available: + +1. Checks if submodule is initialized +2. If not, attempts `git submodule update --init` +3. If git fails, downloads specs directly from GitHub raw content + +### 2. Compile-Time Spec Embedding + +Adapter YAML files are embedded at compile time using `include_str!`: + +```rust +// src/adapters/registry.rs +const HALTECH_NSP_YAML: &str = + include_str!("../../spec/OECUASpecs/adapters/haltech/haltech-nsp.adapter.yaml"); +// ... 7 more adapters + +static ADAPTER_SPECS: LazyLock> = LazyLock::new(|| { + EMBEDDED_ADAPTERS.iter() + .filter_map(|yaml| serde_yaml::from_str(yaml).ok()) + .collect() +}); +``` + +### 2. Normalization Map Building + +A reverse lookup map is built from all adapter `source_names`: + +```rust +// src/adapters/registry.rs +static SPEC_NORMALIZATION_MAP: LazyLock> = LazyLock::new(|| { + let mut map = HashMap::new(); + for adapter in ADAPTER_SPECS.iter() { + for channel in &adapter.channels { + for source_name in &channel.source_names { + map.insert(source_name.to_lowercase(), channel.name.clone()); + } + } + } + map +}); +``` + +### 3. Normalization Integration + +The `normalize.rs` module uses spec normalization as a fallback: + +```rust +// src/normalize.rs +pub fn normalize_channel_name_with_custom( + name: &str, + custom_mappings: Option<&HashMap>, +) -> String { + // 1. Check custom mappings first (highest priority) + // 2. Check built-in hard-coded mappings + // 3. Fall back to OpenECU Alliance spec-based normalization + if let Some(normalized) = adapters::normalize_from_spec(name) { + return normalized; + } + // 4. Return original if no mapping found + name.to_string() +} +``` + +### 4. Channel Metadata Access + +The `Channel` type provides methods that fall back to spec metadata: + +```rust +// src/parsers/types.rs +impl Channel { + /// Get minimum display value - falls back to spec if parser doesn't provide + pub fn display_min(&self) -> Option { + let parser_min = match self { /* ... */ }; + parser_min.or_else(|| self.spec_metadata().and_then(|m| m.min)) + } + + /// Get precision (decimal places) from spec + pub fn precision(&self) -> Option { + self.spec_metadata().and_then(|m| m.precision) + } + + /// Get channel category from spec + pub fn category(&self) -> Option { + self.spec_metadata().map(|m| m.category) + } +} +``` + +## CAN Bus Protocol Support + +In addition to adapter specs (for parsing log files), UltraLog also integrates with OpenECU Alliance protocol specifications. These define CAN bus message structures for real-time ECU data streaming. + +### Protocol vs Adapter Specs + +| Aspect | **Adapters** | **Protocols** | +|--------|-------------|--------------| +| Purpose | Parse saved log files | Real-time CAN bus streaming | +| Format | CSV or binary files | CAN bus messages | +| Use Case | Offline analysis | Live monitoring, dashboards | +| Structure | Columns, headers, timestamps | Messages, signals, bit fields | +| Examples | .csv, .llg, .xrk files | CAN 11-bit/29-bit messages | + +### Protocol Specs + +UltraLog embeds 9 CAN protocol specifications at compile time: + +| Vendor | Protocol ID | Baudrate | Messages | Extended ID | +|--------|-------------|----------|----------|-------------| +| Haltech | haltech-elite-broadcast | 1 Mbps | 50+ | No (11-bit) | +| ECUMaster | ecumaster-emu-broadcast | 1 Mbps | 30+ | No (11-bit) | +| Speeduino | speeduino-broadcast | 500 kbps | 12 | No (11-bit) | +| rusEFI | rusefi-broadcast | 500 kbps | 20+ | No (11-bit) | +| AEM Infinity | aem-infinity-broadcast | 500 kbps | 40+ | Yes (29-bit) | +| Megasquirt | megasquirt-broadcast | 500 kbps | 25+ | Mixed | +| MaxxECU | maxxecu-default | 1 Mbps | 35+ | No (11-bit) | +| Syvecs S7 | syvecs-s7-broadcast | 1 Mbps | 30+ | No (11-bit) | +| Emtron | emtron-broadcast | 1 Mbps | 25+ | No (11-bit) | + +### Protocol Structure + +Each protocol spec defines: + +- **CAN Configuration**: Baudrate, identifier type (11-bit/29-bit), byte order +- **Messages**: CAN message IDs, lengths, broadcast intervals +- **Signals**: Bit-level definitions with start_bit, length, scale, offset +- **Enumerations**: Discrete value mappings (e.g., gear positions) +- **Metadata**: Compatible tools, tested ECU models, known issues + +Example protocol signal definition: + +```yaml +signals: + - name: "RPM" + description: "Engine rotational speed" + start_bit: 0 + length: 16 + byte_order: big_endian + data_type: unsigned + scale: 1.0 + offset: 0 + unit: "rpm" + min: 0 + max: 65535 +``` + +### Protocol API + +```rust +use ultralog::adapters::{ + // Protocol access + get_protocols, // Get all loaded protocol specs + get_protocol_by_id, // Get specific protocol by ID + find_protocols_by_vendor, // Get protocols for a vendor + + // Protocol types + ProtocolSpec, // Top-level protocol definition + ProtocolInfo, // CAN configuration (baudrate, ID type) + MessageSpec, // CAN message definition + SignalSpec, // Signal within a message + EnumSpec, // Enumeration for discrete values + + // Enums + ProtocolType, // can, canfd, lin, k-line + ByteOrder, // little_endian, big_endian + SignalDataType, // unsigned, signed, float, double +}; +``` + +Example usage: + +```rust +// Get all Haltech protocols +let haltech_protos = ultralog::adapters::find_protocols_by_vendor("haltech"); +for proto in haltech_protos { + println!("{}: {} @ {} baud", proto.id, proto.name, proto.protocol.baudrate); + for msg in &proto.messages { + println!(" Message 0x{:X}: {}", msg.id, msg.name); + for signal in &msg.signals { + println!(" {}: {} {}", signal.name, signal.unit.as_deref().unwrap_or(""), signal.description.as_deref().unwrap_or("")); + } + } +} +``` + +## API Reference + +### adapters module + +```rust +use ultralog::adapters::{ + // Normalization + normalize_from_spec, // Normalize channel name using specs + has_spec_normalization, // Check if name has spec normalization + get_spec_normalizations, // Iterator over all (source, display) pairs + + // Metadata + get_channel_metadata, // Get full ChannelMetadata for a name + ChannelMetadata, // Struct with id, name, category, unit, min, max, precision + + // Adapter access + get_adapters, // Get all loaded adapters + get_adapter_by_id, // Get specific adapter by ID + get_adapters_by_vendor, // Get adapters for a vendor + find_adapters_by_extension, // Find adapters supporting file extension + + // Protocol access + get_protocols, // Get all loaded protocol specs + get_protocol_by_id, // Get specific protocol by ID + find_protocols_by_vendor, // Get protocols for a vendor + + // Categories + get_all_categories, // Get all unique ChannelCategory values + get_channels_by_category, // Get all channels for a category + + // Adapter types + AdapterSpec, + ChannelSpec, + ChannelCategory, + DataType, + FileFormatSpec, + + // Protocol types + ProtocolSpec, + ProtocolInfo, + ProtocolType, + MessageSpec, + SignalSpec, + ByteOrder, + SignalDataType, + EnumSpec, +}; +``` + +### normalize module + +```rust +use ultralog::normalize::{ + normalize_channel_name, // Basic normalization + normalize_channel_name_with_custom, // With custom user mappings + get_display_name, // "Normalized (Original)" format + get_spec_metadata, // Get spec metadata for channel + has_normalization, // Check if name can be normalized + get_builtin_mappings, // Get hard-coded mappings + sort_channels_by_priority, // Sort with normalized first +}; +``` + +## Adding New Specs + +1. Add adapter YAML to [OECUASpecs](https://github.com/ClassicMiniDIY/OECUASpecs) repo +2. Update submodule: `git submodule update --remote` +3. Add `include_str!` in `src/adapters/registry.rs` +4. Add to `EMBEDDED_ADAPTERS` array +5. Add download URL to `build.rs` `ADAPTER_SPECS` array +6. Rebuild UltraLog + +Example adding a new adapter: + +```rust +// src/adapters/registry.rs +const NEW_ADAPTER_YAML: &str = + include_str!("../../spec/OECUASpecs/adapters/newvendor/newformat.adapter.yaml"); + +static EMBEDDED_ADAPTERS: &[&str] = &[ + HALTECH_NSP_YAML, + // ... existing adapters ... + NEW_ADAPTER_YAML, // Add here +]; +``` + +## Benefits + +1. **Zero Runtime I/O**: Specs are compiled into the binary +2. **Fast Startup**: LazyLock defers parsing until first use +3. **Type Safety**: Rust types match YAML schema +4. **Comprehensive Metadata**: min/max/precision/category available for all spec channels +5. **Backwards Compatible**: Existing parsers unchanged, spec is additive +6. **Cross-Tool**: Same specs work for any OpenECU Alliance-compatible tool + +## Future Enhancements + +### Runtime Adapter Loading + +Load user adapters from `~/.ultralog/adapters/`: + +```rust +impl AdapterRegistry { + pub fn load_user_adapters(path: &Path) -> Result> { + // Read YAML files from user directory + // Merge with embedded adapters + } +} +``` + +### Generic CSV Parser + +Use adapter specs to drive parsing without format-specific code: + +```rust +pub struct GenericCsvParser { + adapter: AdapterSpec, +} + +impl Parseable for GenericCsvParser { + fn parse(&self, data: &str) -> Result> { + // Use adapter.file_format for delimiter, header row, etc. + // Use adapter.channels for column mapping + // Apply conversion expressions + } +} +``` + +### Unit Conversion from Specs + +Use `source_unit` and `conversion` fields for automatic conversion: + +```yaml +channels: + - id: coolant_temp + unit: celsius + source_unit: fahrenheit + conversion: "(x - 32) * 5/9" +``` + +## Open Questions + +1. **Binary formats**: How much can be spec-driven? + - Current approach: Specs provide metadata, parsers handle binary format details + +2. **User adapter validation**: How to validate user-created adapters? + - Use JSON Schema validation before loading + +3. **Adapter updates**: How to handle spec version changes? + - Include adapter version in metadata, warn on breaking changes diff --git a/UltraLog.code-workspace b/UltraLog.code-workspace index 95515a5..4f26672 100644 --- a/UltraLog.code-workspace +++ b/UltraLog.code-workspace @@ -5,6 +5,12 @@ }, { "path": "../UltraLog.wiki" + }, + { + "path": "../OpenECUAlliance" + }, + { + "path": "../OECUASpecs" } ] } diff --git a/build.rs b/build.rs index a9ddd09..92a5408 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,89 @@ +use std::fs; +use std::io::Write; +use std::path::Path; +use std::process::Command; + +/// Adapter specs to download from GitHub if submodule is missing +const ADAPTER_SPECS: &[(&str, &str)] = &[ + ( + "haltech/haltech-nsp.adapter.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/adapters/haltech/haltech-nsp.adapter.yaml", + ), + ( + "ecumaster/ecumaster-emu-csv.adapter.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/adapters/ecumaster/ecumaster-emu-csv.adapter.yaml", + ), + ( + "link/link-llg.adapter.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/adapters/link/link-llg.adapter.yaml", + ), + ( + "aim/aim-xrk.adapter.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/adapters/aim/aim-xrk.adapter.yaml", + ), + ( + "romraider/romraider-csv.adapter.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/adapters/romraider/romraider-csv.adapter.yaml", + ), + ( + "speeduino/speeduino-mlg.adapter.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/adapters/speeduino/speeduino-mlg.adapter.yaml", + ), + ( + "rusefi/rusefi-mlg.adapter.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/adapters/rusefi/rusefi-mlg.adapter.yaml", + ), + ( + "emerald/emerald-lg.adapter.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/adapters/emerald/emerald-lg.adapter.yaml", + ), +]; + +/// Protocol specs to download from GitHub if submodule is missing +const PROTOCOL_SPECS: &[(&str, &str)] = &[ + ( + "haltech/haltech-elite-broadcast.protocol.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/protocols/haltech/haltech-elite-broadcast.protocol.yaml", + ), + ( + "ecumaster/ecumaster-emu-broadcast.protocol.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/protocols/ecumaster/ecumaster-emu-broadcast.protocol.yaml", + ), + ( + "speeduino/speeduino-broadcast.protocol.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/protocols/speeduino/speeduino-broadcast.protocol.yaml", + ), + ( + "rusefi/rusefi-broadcast.protocol.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/protocols/rusefi/rusefi-broadcast.protocol.yaml", + ), + ( + "aem/aem-infinity-broadcast.protocol.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/protocols/aem/aem-infinity-broadcast.protocol.yaml", + ), + ( + "megasquirt/megasquirt-broadcast.protocol.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/protocols/megasquirt/megasquirt-broadcast.protocol.yaml", + ), + ( + "maxxecu/maxxecu-default.protocol.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/protocols/maxxecu/maxxecu-default.protocol.yaml", + ), + ( + "syvecs/syvecs-s7-broadcast.protocol.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/protocols/syvecs/syvecs-s7-broadcast.protocol.yaml", + ), + ( + "emtron/emtron-broadcast.protocol.yaml", + "https://raw.githubusercontent.com/ClassicMiniDIY/OECUASpecs/main/protocols/emtron/emtron-broadcast.protocol.yaml", + ), +]; + fn main() { + // Ensure OpenECU Alliance specs are available + ensure_adapter_specs(); + ensure_protocol_specs(); + // Only run on Windows #[cfg(windows)] { @@ -19,3 +104,154 @@ fn main() { } } } + +/// Ensure adapter specs are available, either from submodule or by downloading +fn ensure_adapter_specs() { + let spec_dir = Path::new("spec/OECUASpecs/adapters"); + + // Check if submodule is initialized (has the haltech adapter) + let haltech_spec = spec_dir.join("haltech/haltech-nsp.adapter.yaml"); + if haltech_spec.exists() { + println!("cargo:rerun-if-changed=spec/OECUASpecs/adapters"); + return; + } + + println!("cargo:warning=OECUASpecs submodule not initialized, attempting to initialize..."); + + // Try to initialize submodule + let submodule_result = Command::new("git") + .args(["submodule", "update", "--init", "--recursive"]) + .status(); + + if let Ok(status) = submodule_result { + if status.success() && haltech_spec.exists() { + println!("cargo:warning=Successfully initialized OECUASpecs submodule"); + println!("cargo:rerun-if-changed=spec/OECUASpecs/adapters"); + return; + } + } + + // Submodule init failed, try downloading directly + println!("cargo:warning=Submodule init failed, downloading adapter specs from GitHub..."); + download_adapter_specs(spec_dir); +} + +/// Ensure protocol specs are available +fn ensure_protocol_specs() { + let spec_dir = Path::new("spec/OECUASpecs/protocols"); + + // Check if submodule is initialized (has the haltech protocol) + let haltech_spec = spec_dir.join("haltech/haltech-elite-broadcast.protocol.yaml"); + if haltech_spec.exists() { + println!("cargo:rerun-if-changed=spec/OECUASpecs/protocols"); + return; + } + + println!("cargo:warning=Protocol specs not found, downloading from GitHub..."); + download_specs(spec_dir, PROTOCOL_SPECS); +} + +/// Download adapter specs directly from GitHub +fn download_adapter_specs(spec_dir: &Path) { + download_specs(spec_dir, ADAPTER_SPECS); +} + +/// Generic spec downloader +fn download_specs(spec_dir: &Path, specs: &[(&str, &str)]) { + // Create directory structure + for (path, _) in specs { + let full_path = spec_dir.join(path); + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).ok(); + } + } + + // Try curl first (available on most systems) + for (path, url) in specs { + let full_path = spec_dir.join(path); + + if full_path.exists() { + continue; + } + + // Try curl + let result = Command::new("curl") + .args(["-sSL", "-o", full_path.to_str().unwrap(), url]) + .status(); + + if let Ok(status) = result { + if status.success() { + println!( + "cargo:warning=Downloaded {}", + full_path.file_name().unwrap().to_str().unwrap() + ); + continue; + } + } + + // Try wget as fallback + let result = Command::new("wget") + .args(["-q", "-O", full_path.to_str().unwrap(), url]) + .status(); + + if let Ok(status) = result { + if status.success() { + println!( + "cargo:warning=Downloaded {}", + full_path.file_name().unwrap().to_str().unwrap() + ); + continue; + } + } + + // Try PowerShell on Windows + #[cfg(windows)] + { + let ps_script = format!( + "Invoke-WebRequest -Uri '{}' -OutFile '{}'", + url, + full_path.to_str().unwrap() + ); + let result = Command::new("powershell") + .args(["-Command", &ps_script]) + .status(); + + if let Ok(status) = result { + if status.success() { + println!( + "cargo:warning=Downloaded {}", + full_path.file_name().unwrap().to_str().unwrap() + ); + continue; + } + } + } + + // If we get here, all download methods failed + // Create a placeholder that will cause a compile error with helpful message + let error_content = format!( + r#"# ERROR: Failed to download spec from GitHub +# URL: {} +# +# Please run one of the following: +# git submodule update --init +# OR +# curl -sSL -o {} {} +"#, + url, + full_path.display(), + url + ); + + if let Ok(mut file) = fs::File::create(&full_path) { + file.write_all(error_content.as_bytes()).ok(); + } + + println!( + "cargo:warning=Failed to download {}, created placeholder", + path + ); + } + + println!("cargo:rerun-if-changed={}", spec_dir.display()); +} diff --git a/spec/OECUASpecs b/spec/OECUASpecs new file mode 160000 index 0000000..6fe1ecd --- /dev/null +++ b/spec/OECUASpecs @@ -0,0 +1 @@ +Subproject commit 6fe1ecdec33c06d07ed0effb8883cfbf9f0a2d5b diff --git a/src/adapters/mod.rs b/src/adapters/mod.rs new file mode 100644 index 0000000..1b4a15b --- /dev/null +++ b/src/adapters/mod.rs @@ -0,0 +1,36 @@ +//! OpenECU Alliance adapter integration module. +//! +//! This module provides integration with OpenECU Alliance adapter specifications, +//! enabling spec-driven channel normalization, metadata lookup, and format detection. +//! +//! ## Usage +//! +//! ```rust,ignore +//! use ultralog::adapters::{normalize_from_spec, get_channel_metadata}; +//! +//! // Normalize a channel name using spec definitions +//! if let Some(normalized) = normalize_from_spec("Engine Speed") { +//! println!("Normalized: {}", normalized); // "Engine RPM" +//! } +//! +//! // Get full channel metadata +//! if let Some(meta) = get_channel_metadata("TPS") { +//! println!("Category: {}", meta.category.display_name()); +//! println!("Unit: {}", meta.unit); +//! } +//! ``` + +pub mod registry; +pub mod types; + +// Re-export commonly used types and functions +pub use registry::{ + find_adapters_by_extension, find_protocols_by_vendor, get_adapter_by_id, get_adapters, + get_adapters_by_vendor, get_all_categories, get_channel_metadata, get_channels_by_category, + get_protocol_by_id, get_protocols, get_spec_normalizations, has_spec_normalization, + normalize_from_spec, ChannelMetadata, +}; +pub use types::{ + AdapterSpec, ByteOrder, ChannelCategory, ChannelSpec, DataType, EnumSpec, FileFormatSpec, + MessageSpec, ProtocolInfo, ProtocolSpec, ProtocolType, SignalDataType, SignalSpec, +}; diff --git a/src/adapters/registry.rs b/src/adapters/registry.rs new file mode 100644 index 0000000..0c35430 --- /dev/null +++ b/src/adapters/registry.rs @@ -0,0 +1,355 @@ +//! Adapter registry for loading and managing OpenECU Alliance adapter specifications. +//! +//! This module provides functionality to: +//! - Load embedded adapter YAML files at compile time +//! - Build normalization maps from channel source_names +//! - Look up channel metadata by source name + +use std::collections::HashMap; +use std::sync::LazyLock; + +use super::types::{AdapterSpec, ChannelCategory, ChannelSpec, ProtocolSpec}; + +// Embed adapter YAML files at compile time +// These are loaded from the OECUASpecs git submodule (spec/OECUASpecs/) +// If building from source, run: git submodule update --init +const HALTECH_NSP_YAML: &str = + include_str!("../../spec/OECUASpecs/adapters/haltech/haltech-nsp.adapter.yaml"); +const ECUMASTER_EMU_YAML: &str = + include_str!("../../spec/OECUASpecs/adapters/ecumaster/ecumaster-emu-csv.adapter.yaml"); +const LINK_LLG_YAML: &str = + include_str!("../../spec/OECUASpecs/adapters/link/link-llg.adapter.yaml"); +const AIM_XRK_YAML: &str = include_str!("../../spec/OECUASpecs/adapters/aim/aim-xrk.adapter.yaml"); +const ROMRAIDER_CSV_YAML: &str = + include_str!("../../spec/OECUASpecs/adapters/romraider/romraider-csv.adapter.yaml"); +const SPEEDUINO_MLG_YAML: &str = + include_str!("../../spec/OECUASpecs/adapters/speeduino/speeduino-mlg.adapter.yaml"); +const RUSEFI_MLG_YAML: &str = + include_str!("../../spec/OECUASpecs/adapters/rusefi/rusefi-mlg.adapter.yaml"); +const EMERALD_LG_YAML: &str = + include_str!("../../spec/OECUASpecs/adapters/emerald/emerald-lg.adapter.yaml"); + +// Embed protocol YAML files at compile time +// These define CAN bus message structures for real-time data streaming +const HALTECH_ELITE_PROTOCOL_YAML: &str = + include_str!("../../spec/OECUASpecs/protocols/haltech/haltech-elite-broadcast.protocol.yaml"); +const ECUMASTER_EMU_PROTOCOL_YAML: &str = + include_str!("../../spec/OECUASpecs/protocols/ecumaster/ecumaster-emu-broadcast.protocol.yaml"); +const SPEEDUINO_PROTOCOL_YAML: &str = + include_str!("../../spec/OECUASpecs/protocols/speeduino/speeduino-broadcast.protocol.yaml"); +const RUSEFI_PROTOCOL_YAML: &str = + include_str!("../../spec/OECUASpecs/protocols/rusefi/rusefi-broadcast.protocol.yaml"); +const AEM_INFINITY_PROTOCOL_YAML: &str = + include_str!("../../spec/OECUASpecs/protocols/aem/aem-infinity-broadcast.protocol.yaml"); +const MEGASQUIRT_PROTOCOL_YAML: &str = + include_str!("../../spec/OECUASpecs/protocols/megasquirt/megasquirt-broadcast.protocol.yaml"); +const MAXXECU_PROTOCOL_YAML: &str = + include_str!("../../spec/OECUASpecs/protocols/maxxecu/maxxecu-default.protocol.yaml"); +const SYVECS_S7_PROTOCOL_YAML: &str = + include_str!("../../spec/OECUASpecs/protocols/syvecs/syvecs-s7-broadcast.protocol.yaml"); +const EMTRON_PROTOCOL_YAML: &str = + include_str!("../../spec/OECUASpecs/protocols/emtron/emtron-broadcast.protocol.yaml"); + +/// All embedded adapter YAML strings +static EMBEDDED_ADAPTERS: &[&str] = &[ + HALTECH_NSP_YAML, + ECUMASTER_EMU_YAML, + LINK_LLG_YAML, + AIM_XRK_YAML, + ROMRAIDER_CSV_YAML, + SPEEDUINO_MLG_YAML, + RUSEFI_MLG_YAML, + EMERALD_LG_YAML, +]; + +/// All embedded protocol YAML strings +static EMBEDDED_PROTOCOLS: &[&str] = &[ + HALTECH_ELITE_PROTOCOL_YAML, + ECUMASTER_EMU_PROTOCOL_YAML, + SPEEDUINO_PROTOCOL_YAML, + RUSEFI_PROTOCOL_YAML, + AEM_INFINITY_PROTOCOL_YAML, + MEGASQUIRT_PROTOCOL_YAML, + MAXXECU_PROTOCOL_YAML, + SYVECS_S7_PROTOCOL_YAML, + EMTRON_PROTOCOL_YAML, +]; + +/// Parsed adapter specifications (loaded lazily) +static ADAPTER_SPECS: LazyLock> = LazyLock::new(|| { + EMBEDDED_ADAPTERS + .iter() + .filter_map(|yaml| match serde_yaml::from_str(yaml) { + Ok(spec) => Some(spec), + Err(e) => { + tracing::warn!("Failed to parse adapter YAML: {}", e); + None + } + }) + .collect() +}); + +/// Parsed protocol specifications (loaded lazily) +static PROTOCOL_SPECS: LazyLock> = LazyLock::new(|| { + EMBEDDED_PROTOCOLS + .iter() + .filter_map(|yaml| match serde_yaml::from_str(yaml) { + Ok(spec) => Some(spec), + Err(e) => { + tracing::warn!("Failed to parse protocol YAML: {}", e); + None + } + }) + .collect() +}); + +/// Channel metadata lookup by source name (lowercase) +#[derive(Debug, Clone)] +pub struct ChannelMetadata { + /// Canonical channel ID (e.g., "rpm", "coolant_temp") + pub canonical_id: String, + /// Human-readable display name + pub display_name: String, + /// Channel category + pub category: ChannelCategory, + /// Canonical unit + pub unit: String, + /// Minimum valid value + pub min: Option, + /// Maximum valid value + pub max: Option, + /// Decimal places for display + pub precision: Option, + /// Vendor ID that defined this channel + pub vendor: String, +} + +/// Normalization map: source name (lowercase) -> canonical display name +static SPEC_NORMALIZATION_MAP: LazyLock> = LazyLock::new(|| { + let mut map = HashMap::new(); + for adapter in ADAPTER_SPECS.iter() { + for channel in &adapter.channels { + for source_name in &channel.source_names { + // Use display name as the normalized name + map.insert(source_name.to_lowercase(), channel.name.clone()); + } + } + } + map +}); + +/// Channel metadata lookup: source name (lowercase) -> full metadata +static CHANNEL_METADATA_MAP: LazyLock> = LazyLock::new(|| { + let mut map = HashMap::new(); + for adapter in ADAPTER_SPECS.iter() { + for channel in &adapter.channels { + let metadata = ChannelMetadata { + canonical_id: channel.id.clone(), + display_name: channel.name.clone(), + category: channel.category, + unit: channel.unit.clone(), + min: channel.min, + max: channel.max, + precision: channel.precision, + vendor: adapter.vendor.clone(), + }; + for source_name in &channel.source_names { + map.insert(source_name.to_lowercase(), metadata.clone()); + } + } + } + map +}); + +/// Get all loaded adapter specifications +pub fn get_adapters() -> &'static Vec { + &ADAPTER_SPECS +} + +/// Get adapter by ID +pub fn get_adapter_by_id(id: &str) -> Option<&'static AdapterSpec> { + ADAPTER_SPECS.iter().find(|a| a.id == id) +} + +/// Get adapter by vendor name +pub fn get_adapters_by_vendor(vendor: &str) -> Vec<&'static AdapterSpec> { + let vendor_lower = vendor.to_lowercase(); + ADAPTER_SPECS + .iter() + .filter(|a| a.vendor.to_lowercase() == vendor_lower) + .collect() +} + +/// Normalize a channel name using the spec-driven normalization map. +/// Returns the canonical display name if found, otherwise returns the original name. +pub fn normalize_from_spec(name: &str) -> Option { + SPEC_NORMALIZATION_MAP.get(&name.to_lowercase()).cloned() +} + +/// Get channel metadata by source name +pub fn get_channel_metadata(name: &str) -> Option<&'static ChannelMetadata> { + CHANNEL_METADATA_MAP.get(&name.to_lowercase()) +} + +/// Get all spec-based normalization mappings as (source_name, display_name) pairs. +/// This can be used to merge with or enhance the existing normalize.rs mappings. +pub fn get_spec_normalizations() -> impl Iterator { + SPEC_NORMALIZATION_MAP.iter() +} + +/// Check if a channel name has spec-based normalization +pub fn has_spec_normalization(name: &str) -> bool { + SPEC_NORMALIZATION_MAP.contains_key(&name.to_lowercase()) +} + +/// Find matching adapters for a file based on extension +pub fn find_adapters_by_extension(extension: &str) -> Vec<&'static AdapterSpec> { + let ext = if extension.starts_with('.') { + extension.to_lowercase() + } else { + format!(".{}", extension.to_lowercase()) + }; + + ADAPTER_SPECS + .iter() + .filter(|a| { + a.file_format + .extensions + .iter() + .any(|e| e.to_lowercase() == ext) + }) + .collect() +} + +/// Get all unique channel categories from loaded adapters +pub fn get_all_categories() -> Vec { + let mut categories: Vec = ADAPTER_SPECS + .iter() + .flat_map(|a| a.channels.iter().map(|c| c.category)) + .collect(); + categories.sort_by_key(|c| c.display_name()); + categories.dedup(); + categories +} + +/// Get all channels for a specific category across all adapters +pub fn get_channels_by_category(category: ChannelCategory) -> Vec<&'static ChannelSpec> { + ADAPTER_SPECS + .iter() + .flat_map(|a| a.channels.iter()) + .filter(|c| c.category == category) + .collect() +} + +// ============================================================================ +// Protocol Registry Functions +// ============================================================================ + +/// Get all loaded protocol specifications +pub fn get_protocols() -> &'static Vec { + &PROTOCOL_SPECS +} + +/// Get protocol by ID +pub fn get_protocol_by_id(id: &str) -> Option<&'static ProtocolSpec> { + PROTOCOL_SPECS.iter().find(|p| p.id == id) +} + +/// Get protocols by vendor name +pub fn find_protocols_by_vendor(vendor: &str) -> Vec<&'static ProtocolSpec> { + let vendor_lower = vendor.to_lowercase(); + PROTOCOL_SPECS + .iter() + .filter(|p| p.vendor.to_lowercase() == vendor_lower) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_adapters_load() { + let adapters = get_adapters(); + assert!(!adapters.is_empty(), "Should load at least one adapter"); + + // Check that we have the expected adapters + let adapter_ids: Vec<&str> = adapters.iter().map(|a| a.id.as_str()).collect(); + assert!( + adapter_ids.contains(&"haltech-nsp"), + "Should have haltech-nsp adapter" + ); + } + + #[test] + fn test_normalize_rpm() { + // These source names should all normalize to "Engine RPM" based on haltech-nsp spec + let rpm_sources = ["Engine Speed", "Engine RPM", "RPM", "Eng Speed"]; + for source in rpm_sources { + let normalized = normalize_from_spec(source); + assert!(normalized.is_some(), "Should normalize '{}'", source); + assert_eq!( + normalized.unwrap(), + "Engine RPM", + "'{}' should normalize to 'Engine RPM'", + source + ); + } + } + + #[test] + fn test_get_channel_metadata() { + let metadata = get_channel_metadata("Engine Speed"); + assert!(metadata.is_some()); + let meta = metadata.unwrap(); + assert_eq!(meta.canonical_id, "rpm"); + assert_eq!(meta.unit, "rpm"); + assert_eq!(meta.category, ChannelCategory::Engine); + } + + #[test] + fn test_find_adapters_by_extension() { + let csv_adapters = find_adapters_by_extension(".csv"); + assert!(!csv_adapters.is_empty(), "Should find CSV adapters"); + + let llg_adapters = find_adapters_by_extension("llg"); + assert!(!llg_adapters.is_empty(), "Should find LLG adapters"); + } + + #[test] + fn test_protocols_load() { + let protocols = get_protocols(); + assert!(!protocols.is_empty(), "Should load at least one protocol"); + + // Check that we have the expected protocols + let protocol_ids: Vec<&str> = protocols.iter().map(|p| p.id.as_str()).collect(); + assert!( + protocol_ids.contains(&"haltech-elite-broadcast"), + "Should have haltech-elite-broadcast protocol" + ); + } + + #[test] + fn test_get_protocol_by_id() { + let protocol = get_protocol_by_id("haltech-elite-broadcast"); + assert!(protocol.is_some()); + let proto = protocol.unwrap(); + assert_eq!(proto.vendor, "haltech"); + assert!(!proto.messages.is_empty(), "Protocol should have messages"); + } + + #[test] + fn test_find_protocols_by_vendor() { + let haltech_protocols = find_protocols_by_vendor("haltech"); + assert!( + !haltech_protocols.is_empty(), + "Should find Haltech protocols" + ); + + let speeduino_protocols = find_protocols_by_vendor("speeduino"); + assert!( + !speeduino_protocols.is_empty(), + "Should find Speeduino protocols" + ); + } +} diff --git a/src/adapters/types.rs b/src/adapters/types.rs new file mode 100644 index 0000000..9e12320 --- /dev/null +++ b/src/adapters/types.rs @@ -0,0 +1,490 @@ +//! OpenECU Alliance adapter specification types. +//! +//! These types mirror the OpenECU Alliance adapter schema for parsing +//! adapter YAML files that define ECU log format specifications. + +use serde::Deserialize; + +/// OpenECU Alliance adapter specification +#[derive(Debug, Clone, Deserialize)] +pub struct AdapterSpec { + /// Specification version (e.g., "1.0") + pub openecualliance: String, + /// Unique adapter identifier (e.g., "haltech-nsp") + pub id: String, + /// Human-readable adapter name + pub name: String, + /// Adapter version (semver) + pub version: String, + /// ECU vendor/manufacturer + pub vendor: String, + /// Detailed description + #[serde(default)] + pub description: Option, + /// Vendor website URL + #[serde(default)] + pub website: Option, + /// Branding assets + #[serde(default)] + pub branding: Option, + /// File format specification + pub file_format: FileFormatSpec, + /// Channel definitions + pub channels: Vec, + /// Additional metadata + #[serde(default)] + pub metadata: Option, +} + +/// Branding assets for the vendor +#[derive(Debug, Clone, Deserialize, Default)] +pub struct BrandingSpec { + /// Logo file path (relative to assets/logos/) + #[serde(default)] + pub logo: Option, + /// Icon file path (relative to assets/icons/) + #[serde(default)] + pub icon: Option, + /// Banner file path (relative to assets/banners/) + #[serde(default)] + pub banner: Option, + /// Primary brand color (hex) + #[serde(default)] + pub color_primary: Option, + /// Secondary brand color (hex) + #[serde(default)] + pub color_secondary: Option, +} + +/// File format specification +#[derive(Debug, Clone, Deserialize)] +pub struct FileFormatSpec { + /// Format type: "csv" or "binary" + #[serde(rename = "type")] + pub format_type: String, + /// Valid file extensions + pub extensions: Vec, + /// File encoding (CSV only) + #[serde(default)] + pub encoding: Option, + /// Column delimiter (CSV only) + #[serde(default)] + pub delimiter: Option, + /// Header row index (CSV only) + #[serde(default)] + pub header_row: Option, + /// Data start row index (CSV only) + #[serde(default)] + pub data_start_row: Option, + /// Timestamp column name (CSV only) + #[serde(default)] + pub timestamp_column: Option, + /// Timestamp unit (CSV only) + #[serde(default)] + pub timestamp_unit: Option, + /// Byte order: "little" or "big" (binary only) + #[serde(default)] + pub endianness: Option, + /// File signature bytes (binary only) + #[serde(default)] + pub magic_bytes: Option>, + /// Header size in bytes (binary only) + #[serde(default)] + pub header_size: Option, + /// Record size type: "fixed" or "variable" (binary only) + #[serde(default)] + pub record_size: Option, + /// Link to format documentation + #[serde(default)] + pub specification_url: Option, +} + +/// Channel specification +#[derive(Debug, Clone, Deserialize)] +pub struct ChannelSpec { + /// Canonical channel identifier (e.g., "rpm", "coolant_temp") + pub id: String, + /// Human-readable display name + pub name: String, + /// Detailed description + #[serde(default)] + pub description: Option, + /// Channel category + pub category: ChannelCategory, + /// Data type of values + pub data_type: DataType, + /// Canonical unit + pub unit: String, + /// Minimum valid value + #[serde(default)] + pub min: Option, + /// Maximum valid value + #[serde(default)] + pub max: Option, + /// Decimal places for display + #[serde(default)] + pub precision: Option, + /// Vendor-specific names in log files (for normalization) + pub source_names: Vec, + /// Unit of source data if different from canonical + #[serde(default)] + pub source_unit: Option, + /// Formula to convert from source to canonical unit + #[serde(default)] + pub conversion: Option, + /// Searchable tags + #[serde(default)] + pub tags: Option>, +} + +/// Channel categories +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ChannelCategory { + Engine, + Fuel, + Ignition, + Temperature, + Pressure, + Electrical, + Speed, + Drivetrain, + Correction, + System, + Acceleration, + Rotation, + Position, + Suspension, + Timing, + Traction, + DriverInput, + Custom, +} + +impl ChannelCategory { + /// Get display name for the category + pub fn display_name(&self) -> &'static str { + match self { + Self::Engine => "Engine", + Self::Fuel => "Fuel", + Self::Ignition => "Ignition", + Self::Temperature => "Temperature", + Self::Pressure => "Pressure", + Self::Electrical => "Electrical", + Self::Speed => "Speed", + Self::Drivetrain => "Drivetrain", + Self::Correction => "Correction", + Self::System => "System", + Self::Acceleration => "Acceleration", + Self::Rotation => "Rotation", + Self::Position => "Position", + Self::Suspension => "Suspension", + Self::Timing => "Timing", + Self::Traction => "Traction", + Self::DriverInput => "Driver Input", + Self::Custom => "Custom", + } + } +} + +/// Data types for channel values +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DataType { + Float, + Int, + Bool, + String, + Enum, +} + +/// Adapter metadata +#[derive(Debug, Clone, Deserialize, Default)] +pub struct MetadataSpec { + /// Adapter author + #[serde(default)] + pub author: Option, + /// License + #[serde(default)] + pub license: Option, + /// Source repository URL + #[serde(default)] + pub repository: Option, + /// ECU models tested with this adapter + #[serde(default)] + pub tested_with: Option>, + /// Known issues or limitations + #[serde(default)] + pub known_issues: Option>, + /// Changelog entries + #[serde(default)] + pub changelog: Option>, +} + +/// Changelog entry +#[derive(Debug, Clone, Deserialize)] +pub struct ChangelogEntry { + /// Version + pub version: String, + /// Release date + pub date: String, + /// List of changes + pub changes: Vec, +} + +// ============================================================================ +// CAN Protocol Types (for real-time data streaming) +// ============================================================================ + +/// OpenECU Alliance CAN protocol specification +#[derive(Debug, Clone, Deserialize)] +pub struct ProtocolSpec { + /// Specification version (e.g., "1.0") + pub openecualliance: String, + /// Type identifier (must be "protocol") + #[serde(rename = "type")] + pub spec_type: String, + /// Unique protocol identifier (e.g., "haltech-elite-broadcast") + pub id: String, + /// Human-readable protocol name + pub name: String, + /// Protocol version (semver) + pub version: String, + /// ECU vendor/manufacturer + pub vendor: String, + /// Detailed description + #[serde(default)] + pub description: Option, + /// Vendor website URL + #[serde(default)] + pub website: Option, + /// Branding assets + #[serde(default)] + pub branding: Option, + /// Protocol configuration (CAN settings) + pub protocol: ProtocolInfo, + /// CAN message definitions + pub messages: Vec, + /// Enumeration definitions for discrete signals + #[serde(default)] + pub enums: Option>, + /// Additional metadata + #[serde(default)] + pub metadata: Option, +} + +/// Protocol configuration +#[derive(Debug, Clone, Deserialize)] +pub struct ProtocolInfo { + /// Protocol type: "can", "canfd", "lin", "k-line" + #[serde(rename = "type")] + pub protocol_type: ProtocolType, + /// Communication speed in bits per second + pub baudrate: u32, + /// Whether to use 29-bit extended IDs (true) or 11-bit standard IDs (false) + #[serde(default)] + pub extended_id: bool, + /// Data phase baudrate for CAN FD (bits per second) + #[serde(default)] + pub data_baudrate: Option, + /// Whether CAN FD is enabled + #[serde(default)] + pub fd_enabled: bool, + /// Base message ID (if configurable) + #[serde(default)] + pub base_id: Option, + /// Whether base ID can be changed in ECU settings + #[serde(default)] + pub base_id_configurable: bool, +} + +/// Protocol types +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ProtocolType { + Can, + Canfd, + Lin, + #[serde(rename = "k-line")] + KLine, +} + +/// CAN message definition +#[derive(Debug, Clone, Deserialize)] +pub struct MessageSpec { + /// CAN message ID (supports hex strings like "0x360" or decimal integers) + #[serde(deserialize_with = "deserialize_message_id")] + pub id: u32, + /// Human-readable message name + pub name: String, + /// Detailed description + #[serde(default)] + pub description: Option, + /// Message length in bytes (0-8 for CAN, 0-64 for CAN FD) + pub length: u8, + /// Broadcast interval in milliseconds + #[serde(default)] + pub interval_ms: Option, + /// Node that transmits this message + #[serde(default)] + pub transmitter: Option, + /// Signal definitions within this message + pub signals: Vec, +} + +/// Signal within a CAN message +#[derive(Debug, Clone, Deserialize)] +pub struct SignalSpec { + /// Signal name + pub name: String, + /// Detailed description + #[serde(default)] + pub description: Option, + /// Starting bit position (0-indexed) + pub start_bit: u16, + /// Signal length in bits + pub length: u8, + /// Byte order (Intel = little_endian, Motorola = big_endian) + pub byte_order: ByteOrder, + /// Data type interpretation + pub data_type: SignalDataType, + /// Scale factor: physical_value = (raw_value * scale) + offset + #[serde(default = "default_scale")] + pub scale: f64, + /// Offset value: physical_value = (raw_value * scale) + offset + #[serde(default)] + pub offset: f64, + /// Physical unit of the signal + #[serde(default)] + pub unit: Option, + /// Minimum physical value + #[serde(default)] + pub min: Option, + /// Maximum physical value + #[serde(default)] + pub max: Option, + /// Reference to an enum definition for discrete values + #[serde(default)] + pub enum_ref: Option, + /// Additional notes + #[serde(default)] + pub comment: Option, +} + +fn default_scale() -> f64 { + 1.0 +} + +/// Byte order for multi-byte signals +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ByteOrder { + LittleEndian, + BigEndian, +} + +/// Signal data types +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SignalDataType { + Unsigned, + Signed, + Float, + Double, +} + +/// Enumeration definition for discrete signal values +#[derive(Debug, Clone, Deserialize)] +pub struct EnumSpec { + /// Enumeration name + pub name: String, + /// Description of this enumeration + #[serde(default)] + pub description: Option, + /// Mapping of raw values to labels + pub values: std::collections::HashMap, +} + +/// Custom deserializer for message IDs that can be hex strings or integers +fn deserialize_message_id<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::de::{self, Visitor}; + use std::fmt; + + struct MessageIdVisitor; + + impl<'de> Visitor<'de> for MessageIdVisitor { + type Value = u32; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a hex string (0x...) or an integer") + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + if value > u32::MAX as u64 { + Err(E::custom(format!("message ID out of range: {}", value))) + } else { + Ok(value as u32) + } + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + if value < 0 || value > u32::MAX as i64 { + Err(E::custom(format!("message ID out of range: {}", value))) + } else { + Ok(value as u32) + } + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + // Parse hex string (e.g., "0x360" -> 864) + if value.starts_with("0x") || value.starts_with("0X") { + u32::from_str_radix(&value[2..], 16) + .map_err(|e| E::custom(format!("invalid hex string '{}': {}", value, e))) + } else { + value + .parse::() + .map_err(|e| E::custom(format!("invalid integer '{}': {}", value, e))) + } + } + } + + deserializer.deserialize_any(MessageIdVisitor) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_channel_category_display_name() { + assert_eq!(ChannelCategory::Engine.display_name(), "Engine"); + assert_eq!(ChannelCategory::DriverInput.display_name(), "Driver Input"); + } + + #[test] + fn test_message_id_deserialization() { + // Test deserializing from hex string + let json = r#"{"id": "0x360", "name": "Test", "length": 8, "signals": []}"#; + let msg: Result = serde_json::from_str(json); + assert!(msg.is_ok()); + assert_eq!(msg.unwrap().id, 0x360); + + // Test deserializing from decimal integer + let json = r#"{"id": 864, "name": "Test", "length": 8, "signals": []}"#; + let msg: Result = serde_json::from_str(json); + assert!(msg.is_ok()); + assert_eq!(msg.unwrap().id, 864); + } +} diff --git a/src/lib.rs b/src/lib.rs index 3ad7c2c..2c63275 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ //! //! ## Module Structure //! +//! - [`adapters`] - OpenECU Alliance adapter specs for channel normalization //! - [`app`] - Main application state and eframe::App implementation //! - [`parsers`] - ECU log file parsers (Haltech, etc.) //! - [`state`] - Core data types and constants @@ -31,6 +32,7 @@ extern crate rust_i18n; // Fallback to English if a translation is missing i18n!("i18n", fallback = "en"); +pub mod adapters; pub mod analysis; pub mod analytics; pub mod app; diff --git a/src/normalize.rs b/src/normalize.rs index 64f20c9..bebf377 100644 --- a/src/normalize.rs +++ b/src/normalize.rs @@ -2,10 +2,17 @@ //! //! This module provides mappings from various ECU-specific channel names to standardized names, //! making it easier for users to compare data from different logging systems. +//! +//! Normalization is applied in the following priority order: +//! 1. Custom user-defined mappings (highest priority) +//! 2. Built-in hard-coded mappings (this file) +//! 3. OpenECU Alliance spec-based mappings (from adapter source_names) use std::collections::HashMap; use std::sync::LazyLock; +use crate::adapters; + /// Mapping from normalized (standard) names to their possible source names static NORMALIZATION_MAP: LazyLock>> = LazyLock::new(|| { @@ -382,6 +389,18 @@ pub fn normalize_channel_name_with_custom( } } + // Fall back to OpenECU Alliance spec-based normalization + if let Some(normalized) = adapters::normalize_from_spec(name) { + return normalized; + } + + // Try path-stripped version with spec normalization + if let Some(last_segment) = name.rsplit('/').next() { + if let Some(normalized) = adapters::normalize_from_spec(last_segment) { + return normalized; + } + } + // No mapping found, return original name.to_string() } @@ -405,6 +424,12 @@ pub fn get_display_name(name: &str, show_original: bool) -> String { } } +/// Get channel metadata from OpenECU Alliance specs if available. +/// This provides additional information like category, unit, min/max values. +pub fn get_spec_metadata(name: &str) -> Option<&'static adapters::ChannelMetadata> { + adapters::get_channel_metadata(name) +} + /// Check if a channel name has a known normalization mapping. /// Returns true if the name exists in the normalization mappings (built-in or custom). pub fn has_normalization(name: &str, custom_mappings: Option<&HashMap>) -> bool { @@ -437,6 +462,18 @@ pub fn has_normalization(name: &str, custom_mappings: Option<&HashMap &'static str { + let name_lower = name.to_lowercase(); + + // RPM + if name_lower.contains("rpm") || name_lower == "engine speed" { + return "rpm"; + } + + // Temperature (check before pressure due to "temp" being more specific) + if name_lower.contains("temp") + || name_lower.contains("egt") + || name_lower == "iat" + || name_lower == "ect" + || name_lower == "h2o" + || name_lower == "ambient" + { + return "°C"; + } + + // Lambda/AFR + if name_lower.contains("lambda") || name_lower.starts_with("lm") { + return "λ"; + } + if name_lower.contains("afr") || name_lower.contains("a/f") || name_lower.contains("air fuel") { + return "AFR"; + } + + // G-force/Acceleration (check before general patterns) + if name_lower.contains(" g") + || name_lower.starts_with("g ") + || name_lower.contains("lateral g") + || name_lower.contains("longitudinal g") + || name_lower.contains("vertical g") + || name_lower.starts_with("accel") + || name_lower == "g lat" + || name_lower == "g long" + || name_lower == "g vert" + || name_lower == "lat g" + || name_lower == "long g" + { + return "g"; + } + + // Gyroscope/Rotation rate + if name_lower.contains("yaw") + || name_lower.contains("pitch") + || name_lower.contains("roll") + || name_lower.starts_with("gyro") + { + return "°/s"; + } + + // GPS coordinates + if name_lower.contains("latitude") + || name_lower.contains("longitude") + || (name_lower.contains("gps") && name_lower.contains("lat")) + || (name_lower.contains("gps") && name_lower.contains("long")) + || name_lower == "lat" + || name_lower == "lon" + { + return "°"; + } + + // GPS Altitude / Distance + if name_lower.contains("altitude") + || name_lower.contains("distance") + || name_lower.contains("odometer") + || name_lower == "alt" + { + return "m"; + } + + // Heading/Course/Steering angle + if name_lower.contains("heading") + || name_lower.contains("course") + || name_lower.contains("steering") + || name_lower.contains("steer") + { + return "°"; + } + + // Speed (check after steering to avoid conflicts) + if name_lower.contains("speed") || name_lower.contains("veh spd") { + return "km/h"; + } + + // Brake pressure + if name_lower.contains("brake") { + return "bar"; + } + + // Pressure - general (boost, oil, fuel, MAP) + if name_lower.contains("pressure") + || name_lower.contains("press") + || name_lower.contains("boost") + || name_lower == "map" + { + return "bar"; + } + + // Throttle/TPS/Percentage channels + if name_lower.contains("tps") + || name_lower.contains("throttle") + || name_lower.contains("duty") + || name_lower.contains("%") + { + return "%"; + } + + // Voltage/Battery + if name_lower.contains("voltage") + || name_lower.contains("battery") + || name_lower.contains("batt") + || name_lower.contains("vbatt") + || name_lower == "ecu voltage" + { + return "V"; + } + + // Suspension/Damper travel + if name_lower.contains("susp") || name_lower.contains("damper") { + return "mm"; + } + + // Lap time + if name_lower.contains("lap time") || name_lower.contains("laptime") { + return "s"; + } + + // Fuel flow + if name_lower.contains("fuel flow") || name_lower.contains("fuel consumption") { + return "L/h"; + } + + // Satellites count (no unit) + if name_lower.contains("satellite") || name_lower.contains("sats") { + return ""; + } + + // Gear/Lap number (no unit) + if name_lower == "gear" + || name_lower.contains("gear position") + || name_lower == "lap" + || name_lower.contains("lap number") + || name_lower.contains("lapnum") + { + return ""; + } + + // Default: no unit + "" +} + /// AIM log file metadata #[derive(Clone, Debug, Serialize, Default)] pub struct AimMeta { @@ -150,10 +305,9 @@ impl Aim { }; if !name.is_empty() { - channels.push(AimChannel { - name, - unit: String::new(), // Units are not easily extractable from raw binary - }); + // Infer unit from channel name since XRK binary doesn't reliably store units + let unit = infer_unit_from_name(&name).to_string(); + channels.push(AimChannel { name, unit }); } offset = pos + 6; @@ -399,6 +553,75 @@ mod tests { assert_eq!(Aim::read_null_terminated_string(data2, 6), "NoNull"); } + #[test] + fn test_infer_unit_from_name() { + // RPM + assert_eq!(infer_unit_from_name("RPM"), "rpm"); + assert_eq!(infer_unit_from_name("Engine RPM"), "rpm"); + assert_eq!(infer_unit_from_name("Engine Speed"), "rpm"); + + // Temperature + assert_eq!(infer_unit_from_name("Water Temp"), "°C"); + assert_eq!(infer_unit_from_name("Oil Temperature"), "°C"); + assert_eq!(infer_unit_from_name("EGT"), "°C"); + assert_eq!(infer_unit_from_name("IAT"), "°C"); + + // Lambda/AFR + assert_eq!(infer_unit_from_name("Lambda"), "λ"); + assert_eq!(infer_unit_from_name("Lambda 1"), "λ"); + assert_eq!(infer_unit_from_name("AFR"), "AFR"); + assert_eq!(infer_unit_from_name("Air Fuel Ratio"), "AFR"); + + // G-force + assert_eq!(infer_unit_from_name("G Lat"), "g"); + assert_eq!(infer_unit_from_name("Lateral G"), "g"); + assert_eq!(infer_unit_from_name("G Long"), "g"); + assert_eq!(infer_unit_from_name("AccelX"), "g"); + + // Gyro + assert_eq!(infer_unit_from_name("Yaw Rate"), "°/s"); + assert_eq!(infer_unit_from_name("Pitch"), "°/s"); + assert_eq!(infer_unit_from_name("GyroZ"), "°/s"); + + // GPS + assert_eq!(infer_unit_from_name("GPS Lat"), "°"); + assert_eq!(infer_unit_from_name("Latitude"), "°"); + assert_eq!(infer_unit_from_name("Altitude"), "m"); + assert_eq!(infer_unit_from_name("Distance"), "m"); + + // Speed + assert_eq!(infer_unit_from_name("Speed"), "km/h"); + assert_eq!(infer_unit_from_name("Vehicle Speed"), "km/h"); + assert_eq!(infer_unit_from_name("Wheel Speed FL"), "km/h"); + + // Pressure + assert_eq!(infer_unit_from_name("Oil Pressure"), "bar"); + assert_eq!(infer_unit_from_name("Boost"), "bar"); + assert_eq!(infer_unit_from_name("Brake Front"), "bar"); + + // Throttle + assert_eq!(infer_unit_from_name("TPS"), "%"); + assert_eq!(infer_unit_from_name("Throttle Position"), "%"); + + // Voltage + assert_eq!(infer_unit_from_name("Battery"), "V"); + assert_eq!(infer_unit_from_name("Battery Voltage"), "V"); + + // Suspension + assert_eq!(infer_unit_from_name("Susp FL"), "mm"); + assert_eq!(infer_unit_from_name("Damper FR"), "mm"); + + // Timing + assert_eq!(infer_unit_from_name("Lap Time"), "s"); + + // Gear (no unit) + assert_eq!(infer_unit_from_name("Gear"), ""); + assert_eq!(infer_unit_from_name("Lap"), ""); + + // Unknown (no unit) + assert_eq!(infer_unit_from_name("Unknown Channel"), ""); + } + #[test] fn test_parse_all_xrk_files() { let aim_dir = Path::new("exampleLogs/aim"); diff --git a/src/parsers/types.rs b/src/parsers/types.rs index 58b1199..a13ba9d 100644 --- a/src/parsers/types.rs +++ b/src/parsers/types.rs @@ -8,6 +8,7 @@ use super::haltech::{HaltechChannel, HaltechMeta}; use super::link::{LinkChannel, LinkMeta}; use super::romraider::{RomRaiderChannel, RomRaiderMeta}; use super::speeduino::{SpeeduinoChannel, SpeeduinoMeta}; +use crate::adapters::{get_channel_metadata, ChannelCategory, ChannelMetadata}; /// Metadata enum supporting different ECU formats #[derive(Clone, Debug, Serialize, Default)] @@ -107,8 +108,11 @@ impl Channel { } } + /// Get minimum display value for the channel. + /// Falls back to OpenECU Alliance spec metadata if parser doesn't provide min. pub fn display_min(&self) -> Option { - match self { + // First check parser-specific min + let parser_min = match self { Channel::Aim(_) => None, Channel::Emerald(_) => None, Channel::Haltech(h) => h.display_min, @@ -117,11 +121,17 @@ impl Channel { Channel::RomRaider(_) => None, Channel::Speeduino(_) => None, Channel::Computed(_) => None, - } + }; + + // Fall back to spec metadata if parser doesn't provide min + parser_min.or_else(|| self.spec_metadata().and_then(|m| m.min)) } + /// Get maximum display value for the channel. + /// Falls back to OpenECU Alliance spec metadata if parser doesn't provide max. pub fn display_max(&self) -> Option { - match self { + // First check parser-specific max + let parser_max = match self { Channel::Aim(_) => None, Channel::Emerald(_) => None, Channel::Haltech(h) => h.display_max, @@ -130,7 +140,25 @@ impl Channel { Channel::RomRaider(_) => None, Channel::Speeduino(_) => None, Channel::Computed(_) => None, - } + }; + + // Fall back to spec metadata if parser doesn't provide max + parser_max.or_else(|| self.spec_metadata().and_then(|m| m.max)) + } + + /// Get display precision (decimal places) from spec metadata. + pub fn precision(&self) -> Option { + self.spec_metadata().and_then(|m| m.precision) + } + + /// Get channel category from spec metadata. + pub fn category(&self) -> Option { + self.spec_metadata().map(|m| m.category) + } + + /// Get OpenECU Alliance spec metadata for this channel (if available). + pub fn spec_metadata(&self) -> Option<&'static ChannelMetadata> { + get_channel_metadata(&self.name()) } pub fn unit(&self) -> &str { @@ -470,6 +498,7 @@ mod tests { fn test_channel_display_min_max_haltech() { use super::super::haltech::{ChannelType, HaltechChannel}; + // Channel with explicit parser-provided bounds uses those let channel_with_bounds = Channel::Haltech(HaltechChannel { name: "RPM".to_string(), id: "1".to_string(), @@ -481,7 +510,9 @@ mod tests { assert_eq!(channel_with_bounds.display_min(), Some(0.0)); assert_eq!(channel_with_bounds.display_max(), Some(10000.0)); - let channel_without_bounds = Channel::Haltech(HaltechChannel { + // Channel without parser-provided bounds falls back to spec metadata + // "RPM" matches spec source_names, so it gets spec-defined min/max + let channel_with_spec_bounds = Channel::Haltech(HaltechChannel { name: "RPM".to_string(), id: "1".to_string(), r#type: ChannelType::EngineSpeed, @@ -489,8 +520,21 @@ mod tests { display_max: None, }); - assert_eq!(channel_without_bounds.display_min(), None); - assert_eq!(channel_without_bounds.display_max(), None); + // Spec provides min/max for RPM + assert!(channel_with_spec_bounds.display_min().is_some()); + assert!(channel_with_spec_bounds.display_max().is_some()); + + // Channel with name not in specs has no fallback + let channel_no_spec = Channel::Haltech(HaltechChannel { + name: "Custom Proprietary Sensor XYZ".to_string(), + id: "999".to_string(), + r#type: ChannelType::EngineSpeed, + display_min: None, + display_max: None, + }); + + assert_eq!(channel_no_spec.display_min(), None); + assert_eq!(channel_no_spec.display_max(), None); } #[test] @@ -578,4 +622,143 @@ mod tests { let meta_clone = meta.clone(); assert!(matches!(meta_clone, Meta::Empty)); } + + // ============================================ + // Spec Metadata Tests + // ============================================ + + #[test] + fn test_channel_spec_metadata_lookup() { + use super::super::haltech::{ChannelType, HaltechChannel}; + + // Create a channel with a name that matches a spec source_name + let channel = Channel::Haltech(HaltechChannel { + name: "Engine RPM".to_string(), + id: "1".to_string(), + r#type: ChannelType::EngineSpeed, + display_min: None, + display_max: None, + }); + + // Should find spec metadata for "Engine RPM" + let metadata = channel.spec_metadata(); + assert!( + metadata.is_some(), + "Should find spec metadata for Engine RPM" + ); + let meta = metadata.unwrap(); + assert_eq!(meta.canonical_id, "rpm"); + assert_eq!(meta.unit, "rpm"); + } + + #[test] + fn test_channel_display_min_fallback_to_spec() { + use super::super::haltech::{ChannelType, HaltechChannel}; + + // Channel without parser-provided min should fall back to spec + let channel = Channel::Haltech(HaltechChannel { + name: "Engine RPM".to_string(), + id: "1".to_string(), + r#type: ChannelType::EngineSpeed, + display_min: None, + display_max: None, + }); + + // Spec defines min: 0 for rpm + let min = channel.display_min(); + assert!(min.is_some(), "Should get min from spec metadata"); + assert_eq!(min.unwrap(), 0.0); + } + + #[test] + fn test_channel_display_max_fallback_to_spec() { + use super::super::haltech::{ChannelType, HaltechChannel}; + + // Channel without parser-provided max should fall back to spec + let channel = Channel::Haltech(HaltechChannel { + name: "Engine RPM".to_string(), + id: "1".to_string(), + r#type: ChannelType::EngineSpeed, + display_min: None, + display_max: None, + }); + + // Spec defines max for rpm (varies by adapter, but should be > 0) + let max = channel.display_max(); + assert!(max.is_some(), "Should get max from spec metadata"); + assert!(max.unwrap() > 0.0, "Max should be positive"); + } + + #[test] + fn test_channel_parser_min_overrides_spec() { + use super::super::haltech::{ChannelType, HaltechChannel}; + + // Channel with parser-provided min should use that over spec + let channel = Channel::Haltech(HaltechChannel { + name: "Engine RPM".to_string(), + id: "1".to_string(), + r#type: ChannelType::EngineSpeed, + display_min: Some(500.0), // Parser provides custom min + display_max: None, + }); + + let min = channel.display_min(); + assert!(min.is_some()); + assert_eq!(min.unwrap(), 500.0, "Parser min should override spec min"); + } + + #[test] + fn test_channel_precision_from_spec() { + use super::super::haltech::{ChannelType, HaltechChannel}; + + let channel = Channel::Haltech(HaltechChannel { + name: "Engine RPM".to_string(), + id: "1".to_string(), + r#type: ChannelType::EngineSpeed, + display_min: None, + display_max: None, + }); + + // RPM typically has precision of 0 (no decimal places) + let precision = channel.precision(); + assert!(precision.is_some(), "Should get precision from spec"); + assert_eq!(precision.unwrap(), 0, "RPM should have 0 decimal places"); + } + + #[test] + fn test_channel_category_from_spec() { + use super::super::haltech::{ChannelType, HaltechChannel}; + + let channel = Channel::Haltech(HaltechChannel { + name: "Engine RPM".to_string(), + id: "1".to_string(), + r#type: ChannelType::EngineSpeed, + display_min: None, + display_max: None, + }); + + let category = channel.category(); + assert!(category.is_some(), "Should get category from spec"); + assert_eq!(category.unwrap(), ChannelCategory::Engine); + } + + #[test] + fn test_channel_no_spec_metadata_for_unknown() { + use super::super::haltech::{ChannelType, HaltechChannel}; + + // Channel with unknown name should not have spec metadata + let channel = Channel::Haltech(HaltechChannel { + name: "My Custom Unknown Channel XYZ123".to_string(), + id: "999".to_string(), + r#type: ChannelType::Decibel, // Use any type, name won't match spec + display_min: None, + display_max: None, + }); + + assert!(channel.spec_metadata().is_none()); + assert!(channel.display_min().is_none()); + assert!(channel.display_max().is_none()); + assert!(channel.precision().is_none()); + assert!(channel.category().is_none()); + } }