commit b620a8a2b42feddf2acc03b79f9101daba105ffd Author: Piotr Stefański Date: Sat May 16 23:04:58 2026 +0200 Prototype diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..0b2d8b4 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["-C", "target-cpu=x86-64-v3"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b377078 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/target +/_* +/*.ld +/*.ldx +/perf.data* +/*.perf +/flamegraph.svg +/callgrind.out.* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..23cd241 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,636 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atoi_simd" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cdb3708a128e559a30fb830e8a77a5022ee6902806925c216658652b452a44" +dependencies = [ + "debug_unsafe", + "rustversion", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "can-dbc" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2d32f0a7f3754502f84cb68e493a6765ce0978348a59efcb1bccf2ee19c255" +dependencies = [ + "derive-getters", + "nom 7.1.3", +] + +[[package]] +name = "can-dbc" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7a2ee20067a5d9cdcb6df568549a14230a037129713ee20fea6b3fd23e50aa" +dependencies = [ + "can-dbc-pest", + "encoding_rs", + "serde", + "thiserror", +] + +[[package]] +name = "can-dbc-pest" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598a39f5454c5c1b2dfad86f162a0a982aee85d09503b25834a6e9976dcf0eb0" +dependencies = [ + "encoding_rs", + "pest", + "pest_derive", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "codegen" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573800db6c3319bc125ddbf9b9cb001ad1602957f53642ba8d09ff3ddd4da7f1" +dependencies = [ + "indexmap", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dbc-codegen" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb06efcc59895f312b56a604f7ba6a186536b2a52b0b23d828847da4e8086bfc" +dependencies = [ + "anyhow", + "can-dbc 5.0.0", + "heck", +] + +[[package]] +name = "debug_unsafe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eed2c4702fa172d1ce21078faa7c5203e69f5394d48cc436d25928394a867a2" + +[[package]] +name = "derive-getters" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0122f262bf9c9a367829da84f808d9fb128c10ef283bbe7b0922a77cf07b2747" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "rayon", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libsvld" +version = "0.1.0" +dependencies = [ + "atoi_simd", + "bitvec", + "byteorder", + "bytes", + "can-dbc 9.1.0", + "chrono", + "codegen", + "crossbeam-channel", + "dbc-codegen", + "hashbrown", + "memchr", + "memmap2", + "nom 8.0.0", + "page_size", + "positioned-io", + "rayon", + "voracious_radix_sort", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "positioned-io" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ec4b80060f033312b99b6874025d9503d2af87aef2dd4c516e253fbfcdada7" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "svldcli" +version = "0.1.0" +dependencies = [ + "can-dbc 9.1.0", + "libsvld", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "voracious_radix_sort" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446e7ffcb6c27a71d05af7e51ef2ee5b71c48424b122a832f2439651e1914899" +dependencies = [ + "rayon", +] + +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1641b98 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +# [package] +# name = "svld" +# version = "0.1.0" +# edition = "2024" + +[workspace] +resolver = "3" +members = ["libsvld", "svldcli"] + +[profile.release] +lto = "fat" +opt-level = 3 +panic = "abort" + +[profile.release-dbg] +inherits = "release" + +lto = "thin" +opt-level = 3 + +debug = true +panic = "unwind" diff --git a/libsvld/Cargo.toml b/libsvld/Cargo.toml new file mode 100644 index 0000000..dc7b217 --- /dev/null +++ b/libsvld/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "libsvld" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["lib"] +bench = false + +[dependencies] +can-dbc = "9.1.0" +codegen = "0.3.0" +dbc-codegen = "0.3.0" +nom = "8.0.0" +bytes = "1.11.1" +memmap2 = "0.9.10" +memchr = "2.8.0" +crossbeam-channel = "0.5.15" +hashbrown = { version = "0.17.1", features = ["rayon"] } +bitvec = "1.0.1" +byteorder = "1.5.0" +page_size = "0.6.0" +positioned-io = "0.3.5" +atoi_simd = "0.18.1" +voracious_radix_sort = { version = "1.2.0", features = ["voracious_multithread"] } +rayon = "1.12.0" +chrono = { version = "0.4", default-features = false, features = ["std"] } diff --git a/libsvld/src/asc.rs b/libsvld/src/asc.rs new file mode 100644 index 0000000..d3390f5 --- /dev/null +++ b/libsvld/src/asc.rs @@ -0,0 +1,84 @@ +use crate::can_frame::{CanFrame, Direction}; + +#[inline] +fn next_token<'a>(input: &mut &'a [u8]) -> Option<&'a [u8]> { + *input = input.trim_ascii_start(); + if input.is_empty() { + return None; + } + + // Stop on space, tab, or CR. Lines end with CRLF, so stopping on CR keeps + // the cursor on the line; the leading \r\n is then consumed by the next + // call's trim_ascii_start — no need to ever search for '\n'. + let end = memchr::memchr3(b' ', b'\t', b'\r', input).unwrap_or(input.len()); + let token = &input[..end]; + *input = &input[end..]; + Some(token) +} + +pub enum AscLineType { + Frame(CanFrame), + Errorframe, +} + +/// Parse an ASC header date line of the form: +/// `date Sun May 3 08:14:59 pm 2026` +/// +/// Returns a naive (timezone-less) datetime. Format it as ISO 8601 with +/// `dt.format("%Y-%m-%dT%H:%M:%S").to_string()`. +pub fn parse_asc_date(input: &[u8]) -> Option { + let s = std::str::from_utf8(input).ok()?.trim(); + chrono::NaiveDateTime::parse_from_str(s, "date %a %b %e %I:%M:%S %P %Y").ok() +} + +/// Parses one ASC line starting at `input[0]`. +/// +/// Returns the parsed line type and the number of bytes consumed. On success +/// the consumed count leaves `input[consumed..]` pointing at the trailing +/// `\r\n`, which the next call's `trim_ascii_start` will eat. +/// +/// `None` means the line could not be parsed (header line, truncated tail, +/// corrupt frame). The caller is responsible for advancing past the bad line. +pub fn parse_asc_line(input: &[u8]) -> Option<(AscLineType, usize)> { + let mut cursor = input; + + let timestamp: f64 = { + let tok = next_token(&mut cursor)?; + unsafe { std::str::from_utf8_unchecked(tok) }.parse().ok()? + }; + let channel = atoi_simd::parse_pos::(next_token(&mut cursor)?).ok()?; + + let tok = next_token(&mut cursor)?; + if tok[0] == b'E' { + return Some((AscLineType::Errorframe, input.len() - cursor.len())); + } + let can_id = atoi_simd::parse_pos::(tok).ok()?; + + let direction = match next_token(&mut cursor)? { + b"Rx" => Direction::Rx, + b"Tx" => Direction::Tx, + _ => return None, + }; + + // 'D' frame type + next_token(&mut cursor)?; + + let dlc = atoi_simd::parse_pos::(next_token(&mut cursor)?).ok()?; + + let mut data = [0u8; 8]; + for byte in data.iter_mut().take(dlc as usize) { + *byte = atoi_simd::parse_pos::(next_token(&mut cursor)?).ok()?; + } + + Some(( + AscLineType::Frame(CanFrame { + timestamp, + channel, + can_id, + direction, + dlc, + data, + }), + input.len() - cursor.len(), + )) +} diff --git a/libsvld/src/can_frame.rs b/libsvld/src/can_frame.rs new file mode 100644 index 0000000..dda1940 --- /dev/null +++ b/libsvld/src/can_frame.rs @@ -0,0 +1,40 @@ +use voracious_radix_sort::Radixable; + +pub type CanId = u32; + +#[derive(Clone, Debug, PartialEq, Copy)] +pub enum Direction { + Rx, + Tx, +} + +#[derive(Debug, Clone, Copy)] +pub struct CanFrame { + pub timestamp: f64, + pub channel: u8, + pub can_id: CanId, + pub direction: Direction, + pub dlc: u8, + pub data: [u8; 8], +} + +impl PartialOrd for CanFrame { + fn partial_cmp(&self, other: &Self) -> Option { + self.timestamp.partial_cmp(&other.timestamp) + } +} + +// It's fine +impl PartialEq for CanFrame { + fn eq(&self, other: &Self) -> bool { + self.timestamp == other.timestamp + } +} + +impl Radixable for CanFrame { + type Key = f64; + #[inline] + fn key(&self) -> Self::Key { + self.timestamp + } +} diff --git a/libsvld/src/lib.rs b/libsvld/src/lib.rs new file mode 100644 index 0000000..98067d3 --- /dev/null +++ b/libsvld/src/lib.rs @@ -0,0 +1,236 @@ +mod asc; +pub mod can_frame; +mod motec; + +use std::{ + fs::File, + path::Path, + thread, +}; + +use can_dbc::Dbc; +use hashbrown::HashMap; +#[cfg(unix)] +use memmap2::Advice; +use memmap2::Mmap; +use voracious_radix_sort::RadixSort; + +use crate::{ + asc::{parse_asc_date, parse_asc_line}, + can_frame::{CanFrame, CanId}, + motec::{MessageMetadata, SignalMetadata}, +}; + +fn frame_worker( + mmap: &[u8], + dbc: &Dbc, +) -> ( + HashMap>, + HashMap, +) { + let mut channel_data = HashMap::>::new(); + let mut metadatas = HashMap::::new(); + + let get_default = |name: &str| match dbc + .attribute_defaults + .iter() + .find(|x| x.name == name) + .unwrap() + .value + { + can_dbc::AttributeValue::Uint(val) => val, + _ => panic!("Bad {} value type, expected Uint.", name), + }; + + let default_freqency = get_default("Frequency"); + let default_shift = get_default("ld_shift"); + let default_scale = get_default("ld_scale"); + let default_decimal_places = get_default("ld_decimal_places"); + + let mut add_metadata = |id| { + let msg = match dbc.messages.iter().find(|x| x.id.raw() == id) { + Some(msg) => msg, + None => { + eprintln!("Could not find msg with id: {}", id); + return; + } + }; + let signals = &msg.signals; + let mut signals_meta = Vec::::new(); + for signal in signals { + let decimal_places = dbc + .attribute_values_signal + .iter() + .find(|x| { + x.message_id.raw() == id + && x.signal_name == signal.name + && x.name == "ld_decimal_places" + }) + .map_or(default_decimal_places, |x| match x.value { + can_dbc::AttributeValue::Uint(val) => val, + _ => panic!("Bad ld_decimal_places value type, expected Uint."), + }); + + signals_meta.push(SignalMetadata { + shift: default_shift as u16, + scale: default_scale as u16, + decimal_places: decimal_places as u16, + name: signal.name.clone(), + unit: signal.unit.clone(), + signal: signal.clone(), + }); + } + + let frequency = dbc + .attribute_values_message + .iter() + .find(|x| x.message_id.raw() == id && x.name == "Frequency") + .map_or(default_freqency, |x| match x.value { + can_dbc::AttributeValue::Uint(val) => val, + _ => panic!("Bad Frequency value type, expected Uint."), + }); + + metadatas.insert( + id, + MessageMetadata { + id, + frequency: frequency as u16, + signals: signals_meta, + }, + ); + }; + + // Caller has aligned the chunk to line boundaries, so we always start at + // a fresh line. Chunk 0's ASC header lines fail to parse and fall into + // the None branch below, which advances past them. + let mut line_start = 0usize; + + while line_start < mmap.len() { + match parse_asc_line(&mmap[line_start..]) { + Some((asc::AscLineType::Frame(frame), consumed)) => { + match channel_data.get_mut(&frame.can_id) { + Some(vec) => vec.push(frame), + None => { + unsafe { + channel_data.insert_unique_unchecked(frame.can_id, vec![frame]); + } + add_metadata(frame.can_id); + } + } + line_start += consumed; + } + Some((asc::AscLineType::Errorframe, consumed)) => { + line_start += consumed; + } + None => { + // Header lines (chunk 0), corrupt frames, or a truncated tail. + // Walk to the next '\n' by hand — no memchr. + while line_start < mmap.len() && mmap[line_start] != b'\n' { + line_start += 1; + } + line_start += 1; + } + } + } + + (channel_data, metadatas) +} + +pub fn decode(dbc: &Dbc, + asc_path: &Path, + output_dir: &Path) { + let thread_count = std::thread::available_parallelism().unwrap().get(); + + let file = File::open(asc_path).unwrap(); + let mmap = unsafe { Mmap::map(&file).unwrap() }; + + #[cfg(unix)] + mmap.advise(Advice::Sequential).unwrap(); + #[cfg(unix)] + mmap.advise(Advice::WillNeed).unwrap(); + + let date = parse_asc_date(&mmap[..memchr::memchr(b'\r', &mmap).unwrap()]).unwrap(); + + let date = date + .and_utc() + .to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + println!("Date: {}", date); + + let page_size = page_size::get(); + let pages_per_chunk = (mmap.len() / page_size) / thread_count; + let chunk_size = pages_per_chunk * page_size; + + println!("Page size: {}", page_size); + println!("Pages per chunk: {}", pages_per_chunk); + println!("Chunk size: {}", chunk_size); + println!("Thread count: {}", thread_count); + + // Align chunk ends to '\n' so every line is wholly contained in one chunk. + // Without this, the line spanning each boundary is lost on both sides. + let mut bounds: Vec = Vec::with_capacity(thread_count + 1); + bounds.push(0); + let mut pos = 0usize; + while pos + chunk_size < mmap.len() { + let mut p = pos + chunk_size; + // TODO: memchr + while p < mmap.len() && mmap[p] != b'\n' { + p += 1; + } + if p >= mmap.len() { + break; + } + p += 1; // include the '\n' + bounds.push(p); + pos = p; + } + bounds.push(mmap.len()); + + let mut data = HashMap::>::new(); + let mut metadata = HashMap::::new(); + thread::scope(|s| { + let mut handles = vec![]; + for w in bounds.windows(2) { + let mem = &mmap[w[0]..w[1]]; + let dbc = dbc.clone(); + handles.push(s.spawn(move || frame_worker(mem, &dbc))); + } + + for handle in handles { + let (datah, metadatah) = handle.join().unwrap(); + for (id, mut datap) in datah { + match data.get_mut(&id) { + Some(out) => out.append(&mut datap), + None => unsafe { + data.insert_unique_unchecked(id, datap); + }, + } + } + for (id, datap) in metadatah { + if !metadata.contains_key(&id) { + unsafe { + metadata.insert_unique_unchecked(id, datap); + }; + } + } + } + }); + + println!("Post processing"); + + for (_, frames) in &mut data { + // frames.voracious_mt_sort(thread_count); + frames.voracious_sort(); + } + + let msgs: Vec = metadata.values().cloned().collect(); + + println!("Messages: {}", msgs.len()); + println!("Data: {}", data.len()); + + println!("Write to file..."); + // TODO joint paths + let out_file_name = format!("{}.ld", date); + let out_name = Path::new(&out_file_name); + motec::write(&output_dir.join(out_name), &msgs, data); + println!("Finished!"); +} diff --git a/libsvld/src/motec.rs b/libsvld/src/motec.rs new file mode 100644 index 0000000..fdd4197 --- /dev/null +++ b/libsvld/src/motec.rs @@ -0,0 +1,444 @@ +use std::{ + fs::OpenOptions, + io::{Cursor, Seek, SeekFrom, Write}, + path::Path, +}; + +use byteorder::{LittleEndian, WriteBytesExt}; +use can_dbc::{ByteOrder, MultiplexIndicator, Signal, ValueType}; +use hashbrown::HashMap; +use memmap2::MmapMut; +use rayon::prelude::*; + +use crate::can_frame::{CanFrame, CanId}; + +const HEADER_SIZE: usize = 2914; + +const METADATA_SIZE: u32 = 124; +const METADATA_OFFSET: u32 = 2914; + +#[derive(Debug, Clone)] +pub struct SignalMetadata { + pub shift: u16, + pub scale: u16, + pub decimal_places: u16, + pub name: String, + pub unit: String, + pub signal: Signal, +} + +#[derive(Debug, Clone)] +pub struct MessageMetadata { + pub id: CanId, + pub frequency: u16, + pub signals: Vec, +} + +fn write_header(mmap: &mut [u8], metadata_count: u32) { + let mut cursor = Cursor::new(mmap); + + cursor.seek(SeekFrom::Start(0)).unwrap(); + cursor.write_u32::(0x40).unwrap(); + + cursor.seek(SeekFrom::Start(0x08)).unwrap(); + cursor + .write_u32::(HEADER_SIZE as u32) + .unwrap(); + + cursor.seek(SeekFrom::Start(0x0C)).unwrap(); + cursor + .write_u32::(METADATA_OFFSET + (METADATA_SIZE * metadata_count)) + .unwrap(); + + cursor.seek(SeekFrom::Start(0x24)).unwrap(); + cursor.write_u32::(0x6e2).unwrap(); + + cursor.seek(SeekFrom::Start(0x42)).unwrap(); + cursor.write_u16::(0x4240).unwrap(); + + cursor.seek(SeekFrom::Start(0x44)).unwrap(); + cursor.write_u16::(0xf).unwrap(); + + cursor.seek(SeekFrom::Start(0x46)).unwrap(); + cursor.write_u32::(0x2ee7).unwrap(); + + cursor.seek(SeekFrom::Start(0x4a)).unwrap(); + cursor.write_u8(b'A').unwrap(); + cursor.write_u8(b'D').unwrap(); + cursor.write_u8(b'L').unwrap(); + + cursor.seek(SeekFrom::Start(0x52)).unwrap(); + cursor.write_u16::(0x1a4).unwrap(); + + cursor.seek(SeekFrom::Start(0x54)).unwrap(); + cursor.write_u16::(0x80).unwrap(); + + cursor.seek(SeekFrom::Start(0x56)).unwrap(); + cursor.write_u32::(metadata_count).unwrap(); + + cursor.seek(SeekFrom::Start(0x5de)).unwrap(); + cursor.write_u32::(0xd20822).unwrap(); +} + +pub fn get_metadata_total_count( + msgs: &[MessageMetadata], + data: &HashMap>, +) -> usize { + data.iter() + .map(|(id, _)| { + msgs.iter() + .find(|x| x.id == *id) + .map_or(0, |x| x.signals.len()) + }) + .sum() +} + +/// Decode one signal's physical value from a CAN payload (LE-packed into +/// `bytes_le`). Mirrors `calc_bits` + `calc_signed_bits` + factor/offset in +/// _ref_lib/src/process.cc. +#[inline] +fn extract_signal(bytes_le: u64, sig: &Signal) -> f64 { + let size = sig.size; + let shifted = match sig.byte_order { + ByteOrder::BigEndian => bytes_le.swap_bytes() >> (64 - size - sig.start_bit), + ByteOrder::LittleEndian => bytes_le >> sig.start_bit, + }; + let mask = if size >= 64 { + u64::MAX + } else { + (1u64 << size) - 1 + }; + let b = shifted & mask; + let raw = match sig.value_type { + ValueType::Signed => { + if size >= 64 { + b as i64 as f64 + } else { + let sign_bit = 1u64 << (size - 1); + if b & sign_bit != 0 { + ((b | (u64::MAX << size)) as i64) as f64 + } else { + b as f64 + } + } + } + ValueType::Unsigned => b as f64, + }; + raw * sig.factor + sig.offset +} + +/// Resample one message's signals onto a uniform `data_length`-slot grid at +/// `frequency` Hz with forward-fill — see `order_frame` in +/// _ref_lib/src/process.cc:427. Multiplexing is not implemented; every signal +/// is extracted from every payload. +/// +/// Output layout: row-major `grid[j * data_length + k]` for signal `j`, slot `k`. +fn resample( + samples: &[CanFrame], + signals: &[SignalMetadata], + frequency: u16, + last_timestamp: f64, + data_length: usize, +) -> Vec { + let nsig = signals.len(); + let mut grid = vec![0.0f64; nsig * data_length]; + if data_length == 0 { + return grid; + } + let freq = frequency as f64; + let mut vals = vec![0.0f64; nsig]; + let mut frames_ctr: usize = 0; + for frame in samples { + if frame.timestamp > last_timestamp { + continue; + } + let mut new_fc = (frame.timestamp * freq) as usize; + if new_fc >= data_length { + new_fc = data_length - 1; + } + + let bytes_le = u64::from_le_bytes(frame.data); + for j in 0..nsig { + vals[j] = extract_signal(bytes_le, &signals[j].signal); + } + + if new_fc == frames_ctr { + for j in 0..nsig { + grid[j * data_length + frames_ctr] = vals[j]; + } + } else if new_fc > frames_ctr { + for j in 0..nsig { + let row = j * data_length; + let carry = grid[row + frames_ctr]; + grid[row + frames_ctr + 1..row + new_fc].fill(carry); + grid[row + new_fc] = vals[j]; + } + frames_ctr = new_fc; + } + } + // Forward-fill the tail past the last observed sample. + for j in 0..nsig { + if signals[j].signal.multiplexer_indicator != MultiplexIndicator::Plain { + panic!("Multiplex not implemented"); + } + let row = j * data_length; + let carry = grid[row + frames_ctr]; + grid[row + frames_ctr + 1..row + data_length].fill(carry); + } + grid +} + +/// Quantize one signal's resampled grid row into LD's int32-LE representation. +/// Matches `encode_ld_data` in _ref_lib/src/process.cc:608. +#[inline] +fn quantize_signal_into(grid_row: &[f64], signal: &SignalMetadata, dst: &mut [u8]) { + debug_assert_eq!(dst.len(), grid_row.len() * 4); + let inv_scale = 1.0f64 / signal.scale.max(1) as f64; + let p10 = 10f64.powi(signal.decimal_places as i32); + let shift = signal.shift as f64; + for (k, &v) in grid_row.iter().enumerate() { + let q = (v * inv_scale * p10 + shift) as i32; + dst[k * 4..k * 4 + 4].copy_from_slice(&q.to_le_bytes()); + } +} + +/// Fill one 124-byte channel meta record at fixed offsets. The byte layout is +/// opaque LD format; do not interpret beyond "what MoTeC writes here". +fn write_meta_record( + dst: &mut [u8], + prev_offset: u32, + next_offset: u32, + data_offset: u32, + sample_count: u32, + signal_i: u32, + frequency: u16, + signal: &SignalMetadata, +) { + debug_assert_eq!(dst.len(), METADATA_SIZE as usize); + dst[0..4].copy_from_slice(&prev_offset.to_le_bytes()); + dst[4..8].copy_from_slice(&next_offset.to_le_bytes()); + dst[8..12].copy_from_slice(&data_offset.to_le_bytes()); + dst[12..16].copy_from_slice(&sample_count.to_le_bytes()); + dst[16..18].copy_from_slice(&(0x2ee1u16 + signal_i as u16).to_le_bytes()); + dst[18..20].copy_from_slice(&5u16.to_le_bytes()); + dst[20..22].copy_from_slice(&4u16.to_le_bytes()); + dst[22..24].copy_from_slice(&frequency.to_le_bytes()); + dst[24..26].copy_from_slice(&signal.shift.to_le_bytes()); + dst[26..28].copy_from_slice(&1u16.to_le_bytes()); + dst[28..30].copy_from_slice(&signal.scale.to_le_bytes()); + dst[30..32].copy_from_slice(&signal.decimal_places.to_le_bytes()); + + let nb = signal.name.as_bytes(); + let nl = nb.len().min(31); + dst[32..32 + nl].copy_from_slice(&nb[..nl]); + let sl = nb.len().min(7); + dst[64..64 + sl].copy_from_slice(&nb[..sl]); + + let ub = signal.unit.as_bytes(); + let ul = ub.len().min(7); + dst[72..72 + ul].copy_from_slice(&ub[..ul]); +} + +/// Per-message output plan: where its meta records and sample data go in the +/// final file, and what global signal index it starts at. Built sequentially +/// so all offsets are deterministic; the actual work can then run in parallel. +struct MsgPlan<'a> { + msg: &'a MessageMetadata, + samples: &'a [CanFrame], + data_length: usize, + /// Global signal index of this message's first signal. + base_signal_i: u32, + /// Absolute file offset of this message's first signal's data block. + first_data_offset: u32, +} + +impl<'a> MsgPlan<'a> { + #[inline] + fn meta_bytes(&self) -> usize { + self.msg.signals.len() * METADATA_SIZE as usize + } + #[inline] + fn data_bytes(&self) -> usize { + self.msg.signals.len() * self.data_length * 4 + } +} + +/// Walk `msgs` in order, assign per-message data offsets and starting signal +/// indices. Messages with no recorded samples are skipped — they contribute +/// nothing to the file. +fn plan_layout<'a>( + msgs: &'a [MessageMetadata], + data: &'a HashMap>, + metadata_total_count: u32, + last_timestamp: f64, +) -> Vec> { + let mut plans = Vec::with_capacity(msgs.len()); + let mut signal_i: u32 = 0; + let mut data_off = METADATA_OFFSET + METADATA_SIZE * metadata_total_count; + for msg in msgs { + let samples = match data.get(&msg.id) { + Some(s) => s.as_slice(), + None => continue, + }; + let data_length = (last_timestamp * msg.frequency as f64) as usize; + let plan = MsgPlan { + msg, + samples, + data_length, + base_signal_i: signal_i, + first_data_offset: data_off, + }; + signal_i += msg.signals.len() as u32; + data_off += plan.data_bytes() as u32; + plans.push(plan); + } + plans +} + +/// Resample, quantize, and emit meta records for one message. `meta_dst` and +/// `data_dst` are exact-sized disjoint slices of the final output, so this +/// can run in parallel across messages with no locking. +fn process_message(plan: &MsgPlan, last_timestamp: f64, meta_dst: &mut [u8], data_dst: &mut [u8]) { + let nsig = plan.msg.signals.len(); + let data_length = plan.data_length; + let signals = &plan.msg.signals; + let frequency = plan.msg.frequency; + + let grid = resample( + plan.samples, + signals, + frequency, + last_timestamp, + data_length, + ); + + let row_bytes = data_length * 4; + let sample_count = data_length as u32; + for j in 0..nsig { + let signal_i = plan.base_signal_i + j as u32; + let prev_offset = if signal_i == 0 { + 0 + } else { + METADATA_OFFSET + (signal_i - 1) * METADATA_SIZE + }; + let next_offset = METADATA_OFFSET + (signal_i + 1) * METADATA_SIZE; + let data_offset = plan.first_data_offset + (j as u32) * row_bytes as u32; + + let grid_row = &grid[j * data_length..(j + 1) * data_length]; + let data_slot = &mut data_dst[j * row_bytes..(j + 1) * row_bytes]; + quantize_signal_into(grid_row, &signals[j], data_slot); + + let meta_start = j * METADATA_SIZE as usize; + let meta_slot = &mut meta_dst[meta_start..meta_start + METADATA_SIZE as usize]; + write_meta_record( + meta_slot, + prev_offset, + next_offset, + data_offset, + sample_count, + signal_i, + frequency, + &signals[j], + ); + } +} + +/// Carve a pre-allocated buffer into per-plan disjoint mutable slices. Lengths +/// come from `len_of(plan)`; the slices appear in `plans` order. +fn split_by_plan<'b, 'a: 'b, F>( + buf: &'b mut [u8], + plans: &[MsgPlan<'a>], + len_of: F, +) -> Vec<&'b mut [u8]> +where + F: Fn(&MsgPlan<'a>) -> usize, +{ + let mut slices = Vec::with_capacity(plans.len()); + let mut rest = buf; + for p in plans { + let (head, tail) = rest.split_at_mut(len_of(p)); + slices.push(head); + rest = tail; + } + slices +} + +fn write_ldx(outpath: &Path, last_timestamp: f64) -> std::io::Result<()> { + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(outpath)?; + + let minutes = (last_timestamp as u64) / 60; + let seconds = last_timestamp - (minutes as f64 * 60.0); + let fastest_time = format!("{}:{}", minutes, seconds); + + write!(file, + " + + + + + + + + +
+ + + +
+
+
+", fastest_time)?; + + Ok(()) +} + +pub fn write(outfile: &Path, msgs: &[MessageMetadata], data: HashMap>) { + let metadata_total_count = get_metadata_total_count(msgs, &data) as u32; + + // Samples per CAN id are already time-sorted, so `last()` is the per-channel max. + let last_timestamp: f64 = data + .par_values() + .filter_map(|v| v.last()) + .map(|f| f.timestamp) + .reduce(|| 0.0f64, f64::max); + + let plans = plan_layout(msgs, &data, metadata_total_count, last_timestamp); + + let meta_total = METADATA_SIZE as usize * metadata_total_count as usize; + let data_total: usize = plans.iter().map(|p| p.data_bytes()).sum(); + + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(outfile) + .unwrap(); + file.set_len((HEADER_SIZE + meta_total + data_total) as u64) + .unwrap(); + + let mut mmap = unsafe { MmapMut::map_mut(&file).unwrap() }; + + write_header(&mut mmap, metadata_total_count); + + let tailbuff = &mut mmap[HEADER_SIZE..]; + + let (meta_region, data_region) = tailbuff.split_at_mut(meta_total); + + let meta_slices = split_by_plan(meta_region, &plans, |p| p.meta_bytes()); + let data_slices = split_by_plan(data_region, &plans, |p| p.data_bytes()); + + plans + .par_iter() + .zip(meta_slices.into_par_iter()) + .zip(data_slices.into_par_iter()) + .for_each(|((plan, meta), data)| { + process_message(plan, last_timestamp, meta, data); + }); + + write_ldx(&outfile.with_extension("ldx"), last_timestamp).unwrap(); +} diff --git a/svldcli/Cargo.toml b/svldcli/Cargo.toml new file mode 100644 index 0000000..25c7375 --- /dev/null +++ b/svldcli/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "svldcli" +version = "0.1.0" +edition = "2024" + +[dependencies] +libsvld = { path = "../libsvld" } +can-dbc = "9.1.0" diff --git a/svldcli/src/main.rs b/svldcli/src/main.rs new file mode 100644 index 0000000..0a312ea --- /dev/null +++ b/svldcli/src/main.rs @@ -0,0 +1,15 @@ +use std::{fs, path::Path}; + +use can_dbc::Dbc; + +fn main() { + let data = + fs::read_to_string("./_reference/RT15evo_v8.dbc").expect("Unable to read input file"); + let dbc = Dbc::try_from(data.as_str()).expect("Failed to parse dbc file"); + + let args: Vec::<_> = std::env::args().collect(); + let asc_path = Path::new(&args[1]); + let output_dir = Path::new(&args[2]); + + libsvld::decode(&dbc, asc_path, output_dir); +}