From fa42a897fe559cf4c2ba483b0a48840642c0152a Mon Sep 17 00:00:00 2001 From: heroichornet Date: Sun, 2 Jun 2024 21:57:30 +0200 Subject: [PATCH 01/29] starting wasm feature --- wasm/m-bus-parser-wasm-pack/.appveyor.yml | 11 +++ .../.github/dependabot.yml | 8 +++ wasm/m-bus-parser-wasm-pack/.gitignore | 6 ++ wasm/m-bus-parser-wasm-pack/.travis.yml | 69 +++++++++++++++++++ wasm/m-bus-parser-wasm-pack/Cargo.toml | 35 ++++++++++ wasm/m-bus-parser-wasm-pack/README.md | 3 + wasm/m-bus-parser-wasm-pack/src/lib.rs | 16 +++++ wasm/m-bus-parser-wasm-pack/src/utils.rs | 10 +++ wasm/m-bus-parser-wasm-pack/tests/web.rs | 13 ++++ 9 files changed, 171 insertions(+) create mode 100644 wasm/m-bus-parser-wasm-pack/.appveyor.yml create mode 100644 wasm/m-bus-parser-wasm-pack/.github/dependabot.yml create mode 100644 wasm/m-bus-parser-wasm-pack/.gitignore create mode 100644 wasm/m-bus-parser-wasm-pack/.travis.yml create mode 100644 wasm/m-bus-parser-wasm-pack/Cargo.toml create mode 100644 wasm/m-bus-parser-wasm-pack/README.md create mode 100644 wasm/m-bus-parser-wasm-pack/src/lib.rs create mode 100644 wasm/m-bus-parser-wasm-pack/src/utils.rs create mode 100644 wasm/m-bus-parser-wasm-pack/tests/web.rs diff --git a/wasm/m-bus-parser-wasm-pack/.appveyor.yml b/wasm/m-bus-parser-wasm-pack/.appveyor.yml new file mode 100644 index 0000000..50910bd --- /dev/null +++ b/wasm/m-bus-parser-wasm-pack/.appveyor.yml @@ -0,0 +1,11 @@ +install: + - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe + - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly + - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin + - rustc -V + - cargo -V + +build: false + +test_script: + - cargo test --locked diff --git a/wasm/m-bus-parser-wasm-pack/.github/dependabot.yml b/wasm/m-bus-parser-wasm-pack/.github/dependabot.yml new file mode 100644 index 0000000..7377d37 --- /dev/null +++ b/wasm/m-bus-parser-wasm-pack/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: cargo + directory: "/" + schedule: + interval: daily + time: "08:00" + open-pull-requests-limit: 10 diff --git a/wasm/m-bus-parser-wasm-pack/.gitignore b/wasm/m-bus-parser-wasm-pack/.gitignore new file mode 100644 index 0000000..4e30131 --- /dev/null +++ b/wasm/m-bus-parser-wasm-pack/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log diff --git a/wasm/m-bus-parser-wasm-pack/.travis.yml b/wasm/m-bus-parser-wasm-pack/.travis.yml new file mode 100644 index 0000000..7a91325 --- /dev/null +++ b/wasm/m-bus-parser-wasm-pack/.travis.yml @@ -0,0 +1,69 @@ +language: rust +sudo: false + +cache: cargo + +matrix: + include: + + # Builds with wasm-pack. + - rust: beta + env: RUST_BACKTRACE=1 + addons: + firefox: latest + chrome: stable + before_script: + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) + - cargo install-update -a + - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f + script: + - cargo generate --git . --name testing + # Having a broken Cargo.toml (in that it has curlies in fields) anywhere + # in any of our parent dirs is problematic. + - mv Cargo.toml Cargo.toml.tmpl + - cd testing + - wasm-pack build + - wasm-pack test --chrome --firefox --headless + + # Builds on nightly. + - rust: nightly + env: RUST_BACKTRACE=1 + before_script: + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) + - cargo install-update -a + - rustup target add wasm32-unknown-unknown + script: + - cargo generate --git . --name testing + - mv Cargo.toml Cargo.toml.tmpl + - cd testing + - cargo check + - cargo check --target wasm32-unknown-unknown + - cargo check --no-default-features + - cargo check --target wasm32-unknown-unknown --no-default-features + - cargo check --no-default-features --features console_error_panic_hook + - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook + - cargo check --no-default-features --features "console_error_panic_hook wee_alloc" + - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" + + # Builds on beta. + - rust: beta + env: RUST_BACKTRACE=1 + before_script: + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) + - cargo install-update -a + - rustup target add wasm32-unknown-unknown + script: + - cargo generate --git . --name testing + - mv Cargo.toml Cargo.toml.tmpl + - cd testing + - cargo check + - cargo check --target wasm32-unknown-unknown + - cargo check --no-default-features + - cargo check --target wasm32-unknown-unknown --no-default-features + - cargo check --no-default-features --features console_error_panic_hook + - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook + # Note: no enabling the `wee_alloc` feature here because it requires + # nightly for now. diff --git a/wasm/m-bus-parser-wasm-pack/Cargo.toml b/wasm/m-bus-parser-wasm-pack/Cargo.toml new file mode 100644 index 0000000..61e1cd7 --- /dev/null +++ b/wasm/m-bus-parser-wasm-pack/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "m-bus-parser-wasm-pack" +version = "0.0.0" +edition = "2021" +description = "A wasm-pack to use the library for parsing M-Bus frames" +license = "MIT" +homepage = "https://maebli.github.io/" +repository = "https://github.com/maebli/m-bus-parser" +readme = "README.md" +authors = ["Michael Aebli"] +keywords = ["m-bus", "parser", "parse", "wasm-pack"] + + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2.84" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.7", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" +m-bus-parser = { path = "..", version = "0.0.7", features = ["std"] } + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/wasm/m-bus-parser-wasm-pack/README.md b/wasm/m-bus-parser-wasm-pack/README.md new file mode 100644 index 0000000..528b8a6 --- /dev/null +++ b/wasm/m-bus-parser-wasm-pack/README.md @@ -0,0 +1,3 @@ +# WASM Package for m-bus-parser (wired) + +This package is a WebAssembly (WASM) package for the mbus parser so it may be used in the browser for example. \ No newline at end of file diff --git a/wasm/m-bus-parser-wasm-pack/src/lib.rs b/wasm/m-bus-parser-wasm-pack/src/lib.rs new file mode 100644 index 0000000..149805a --- /dev/null +++ b/wasm/m-bus-parser-wasm-pack/src/lib.rs @@ -0,0 +1,16 @@ +mod utils; +use m_bus_parser; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern "C" { + fn alert(s: &str); +} + +#[wasm_bindgen] +pub fn parse(s: &str) -> String { + // remove white space + let s = s.trim().replace([' ', "0x", '\\'], ""); + m_bus_parser.parse(s); +} diff --git a/wasm/m-bus-parser-wasm-pack/src/utils.rs b/wasm/m-bus-parser-wasm-pack/src/utils.rs new file mode 100644 index 0000000..b1d7929 --- /dev/null +++ b/wasm/m-bus-parser-wasm-pack/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/wasm/m-bus-parser-wasm-pack/tests/web.rs b/wasm/m-bus-parser-wasm-pack/tests/web.rs new file mode 100644 index 0000000..de5c1da --- /dev/null +++ b/wasm/m-bus-parser-wasm-pack/tests/web.rs @@ -0,0 +1,13 @@ +//! Test suite for the Web and headless browsers. + +#![cfg(target_arch = "wasm32")] + +extern crate wasm_bindgen_test; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn pass() { + assert_eq!(1 + 1, 2); +} From ea594d3ddd20fdb10cadc438c54c140f8855311f Mon Sep 17 00:00:00 2001 From: heroichornet Date: Sun, 9 Jun 2024 12:40:16 +0200 Subject: [PATCH 02/29] fixing clippy error --- src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6dae634..32c7316 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,9 +77,7 @@ impl From for MbusError { impl From for MbusError { fn from(error: ApplicationLayerError) -> MbusError { - match error { - _ => MbusError::ApplicationLayerError(error), - } + MbusError::ApplicationLayerError(error) } } From 2bdaf58151ba1719c114b5c941501a2369c58a5c Mon Sep 17 00:00:00 2001 From: heroichornet Date: Mon, 10 Jun 2024 22:42:48 +0200 Subject: [PATCH 03/29] moving module up --- Cargo.toml | 2 +- cli/Cargo.toml | 2 +- wasm/{m-bus-parser-wasm-pack => }/Cargo.toml | 2 +- wasm/{m-bus-parser-wasm-pack => }/README.md | 0 wasm/m-bus-parser-wasm-pack/.appveyor.yml | 11 --- .../.github/dependabot.yml | 8 --- wasm/m-bus-parser-wasm-pack/.gitignore | 6 -- wasm/m-bus-parser-wasm-pack/.travis.yml | 69 ------------------- wasm/{m-bus-parser-wasm-pack => }/src/lib.rs | 9 ++- .../{m-bus-parser-wasm-pack => }/src/utils.rs | 0 .../{m-bus-parser-wasm-pack => }/tests/web.rs | 0 11 files changed, 7 insertions(+), 102 deletions(-) rename wasm/{m-bus-parser-wasm-pack => }/Cargo.toml (93%) rename wasm/{m-bus-parser-wasm-pack => }/README.md (100%) delete mode 100644 wasm/m-bus-parser-wasm-pack/.appveyor.yml delete mode 100644 wasm/m-bus-parser-wasm-pack/.github/dependabot.yml delete mode 100644 wasm/m-bus-parser-wasm-pack/.gitignore delete mode 100644 wasm/m-bus-parser-wasm-pack/.travis.yml rename wasm/{m-bus-parser-wasm-pack => }/src/lib.rs (66%) rename wasm/{m-bus-parser-wasm-pack => }/src/utils.rs (100%) rename wasm/{m-bus-parser-wasm-pack => }/tests/web.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index dfcaa5d..1d710a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,4 +39,4 @@ bitflags = "2.4.2" arrayvec = "0.7.4" [workspace] -members = ["cli"] +members = ["cli", "wasm"] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0499778..d493a26 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "m-bus-parser-cli" -version = "0.0.4" +version = "0.0.3" edition = "2021" description = "A cli to use the library for parsing M-Bus frames" license = "MIT" diff --git a/wasm/m-bus-parser-wasm-pack/Cargo.toml b/wasm/Cargo.toml similarity index 93% rename from wasm/m-bus-parser-wasm-pack/Cargo.toml rename to wasm/Cargo.toml index 61e1cd7..b4f2c00 100644 --- a/wasm/m-bus-parser-wasm-pack/Cargo.toml +++ b/wasm/Cargo.toml @@ -19,6 +19,7 @@ default = ["console_error_panic_hook"] [dependencies] wasm-bindgen = "0.2.84" +m-bus-parser = { path = "..", version = "0.0.9", features = ["std"] } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires @@ -28,7 +29,6 @@ console_error_panic_hook = { version = "0.1.7", optional = true } [dev-dependencies] wasm-bindgen-test = "0.3.34" -m-bus-parser = { path = "..", version = "0.0.7", features = ["std"] } [profile.release] # Tell `rustc` to optimize for small code size. diff --git a/wasm/m-bus-parser-wasm-pack/README.md b/wasm/README.md similarity index 100% rename from wasm/m-bus-parser-wasm-pack/README.md rename to wasm/README.md diff --git a/wasm/m-bus-parser-wasm-pack/.appveyor.yml b/wasm/m-bus-parser-wasm-pack/.appveyor.yml deleted file mode 100644 index 50910bd..0000000 --- a/wasm/m-bus-parser-wasm-pack/.appveyor.yml +++ /dev/null @@ -1,11 +0,0 @@ -install: - - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe - - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly - - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin - - rustc -V - - cargo -V - -build: false - -test_script: - - cargo test --locked diff --git a/wasm/m-bus-parser-wasm-pack/.github/dependabot.yml b/wasm/m-bus-parser-wasm-pack/.github/dependabot.yml deleted file mode 100644 index 7377d37..0000000 --- a/wasm/m-bus-parser-wasm-pack/.github/dependabot.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 2 -updates: -- package-ecosystem: cargo - directory: "/" - schedule: - interval: daily - time: "08:00" - open-pull-requests-limit: 10 diff --git a/wasm/m-bus-parser-wasm-pack/.gitignore b/wasm/m-bus-parser-wasm-pack/.gitignore deleted file mode 100644 index 4e30131..0000000 --- a/wasm/m-bus-parser-wasm-pack/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/target -**/*.rs.bk -Cargo.lock -bin/ -pkg/ -wasm-pack.log diff --git a/wasm/m-bus-parser-wasm-pack/.travis.yml b/wasm/m-bus-parser-wasm-pack/.travis.yml deleted file mode 100644 index 7a91325..0000000 --- a/wasm/m-bus-parser-wasm-pack/.travis.yml +++ /dev/null @@ -1,69 +0,0 @@ -language: rust -sudo: false - -cache: cargo - -matrix: - include: - - # Builds with wasm-pack. - - rust: beta - env: RUST_BACKTRACE=1 - addons: - firefox: latest - chrome: stable - before_script: - - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) - - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) - - cargo install-update -a - - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f - script: - - cargo generate --git . --name testing - # Having a broken Cargo.toml (in that it has curlies in fields) anywhere - # in any of our parent dirs is problematic. - - mv Cargo.toml Cargo.toml.tmpl - - cd testing - - wasm-pack build - - wasm-pack test --chrome --firefox --headless - - # Builds on nightly. - - rust: nightly - env: RUST_BACKTRACE=1 - before_script: - - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) - - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) - - cargo install-update -a - - rustup target add wasm32-unknown-unknown - script: - - cargo generate --git . --name testing - - mv Cargo.toml Cargo.toml.tmpl - - cd testing - - cargo check - - cargo check --target wasm32-unknown-unknown - - cargo check --no-default-features - - cargo check --target wasm32-unknown-unknown --no-default-features - - cargo check --no-default-features --features console_error_panic_hook - - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook - - cargo check --no-default-features --features "console_error_panic_hook wee_alloc" - - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" - - # Builds on beta. - - rust: beta - env: RUST_BACKTRACE=1 - before_script: - - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) - - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) - - cargo install-update -a - - rustup target add wasm32-unknown-unknown - script: - - cargo generate --git . --name testing - - mv Cargo.toml Cargo.toml.tmpl - - cd testing - - cargo check - - cargo check --target wasm32-unknown-unknown - - cargo check --no-default-features - - cargo check --target wasm32-unknown-unknown --no-default-features - - cargo check --no-default-features --features console_error_panic_hook - - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook - # Note: no enabling the `wee_alloc` feature here because it requires - # nightly for now. diff --git a/wasm/m-bus-parser-wasm-pack/src/lib.rs b/wasm/src/lib.rs similarity index 66% rename from wasm/m-bus-parser-wasm-pack/src/lib.rs rename to wasm/src/lib.rs index 149805a..7a61a54 100644 --- a/wasm/m-bus-parser-wasm-pack/src/lib.rs +++ b/wasm/src/lib.rs @@ -1,12 +1,11 @@ mod utils; use m_bus_parser; -use wasm_bindgen::prelude::*; +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; -#[wasm_bindgen] -extern "C" { - fn alert(s: &str); -} +use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn parse(s: &str) -> String { diff --git a/wasm/m-bus-parser-wasm-pack/src/utils.rs b/wasm/src/utils.rs similarity index 100% rename from wasm/m-bus-parser-wasm-pack/src/utils.rs rename to wasm/src/utils.rs diff --git a/wasm/m-bus-parser-wasm-pack/tests/web.rs b/wasm/tests/web.rs similarity index 100% rename from wasm/m-bus-parser-wasm-pack/tests/web.rs rename to wasm/tests/web.rs From 6270206bcb1f4d528973c10d0b76b5e2654b9bf4 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Mon, 10 Jun 2024 23:36:55 +0200 Subject: [PATCH 04/29] wasm looking better --- wasm/Cargo.toml | 7 +++++++ wasm/src/lib.rs | 33 ++++++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index b4f2c00..58a1077 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -27,6 +27,13 @@ m-bus-parser = { path = "..", version = "0.0.9", features = ["std"] } # code size when deploying. console_error_panic_hook = { version = "0.1.7", optional = true } +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.5", optional = true } + [dev-dependencies] wasm-bindgen-test = "0.3.34" diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 7a61a54..0130e7f 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -1,5 +1,5 @@ mod utils; -use m_bus_parser; +use m_bus_parser::{self, MbusData}; #[cfg(feature = "wee_alloc")] #[global_allocator] @@ -8,8 +8,31 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; use wasm_bindgen::prelude::*; #[wasm_bindgen] -pub fn parse(s: &str) -> String { - // remove white space - let s = s.trim().replace([' ', "0x", '\\'], ""); - m_bus_parser.parse(s); +extern "C" { + fn alert(s: &str); +} + +fn clean_and_convert(input: &str) -> Vec { + let input = input.trim(); + let cleaned_data: String = input.replace("0x", "").replace([' ', ','], ""); + + // Convert pairs of characters into bytes + cleaned_data + .as_bytes() + .chunks(2) + .map(|chunk| { + let byte_str = std::str::from_utf8(chunk).expect("Invalid UTF-8 sequence"); + u8::from_str_radix(byte_str, 16).expect("Invalid byte value") + }) + .collect() +} + +#[wasm_bindgen] +pub fn m_bus_parse(s: &str) -> String { + let s = clean_and_convert(s); + if let Ok(mbus_data) = MbusData::try_from(s.as_slice()) { + format!("{:?}", mbus_data) + } else { + format!("Failed to parse") + } } From 9ee8af92e78dc45d6bb4c305181fffcdadcf6163 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Tue, 11 Jun 2024 19:44:14 +0200 Subject: [PATCH 05/29] improving "full parse" function to be re-used in wasm and also in cli --- src/lib.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 32c7316..89b6dcb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,7 @@ pub mod user_data; pub struct MbusData<'a> { pub frame: frames::Frame<'a>, pub user_data: Option>, + pub data_records: Option, } #[derive(Debug)] @@ -86,15 +87,30 @@ impl<'a> TryFrom<&'a [u8]> for MbusData<'a> { fn try_from(data: &'a [u8]) -> Result { let frame = frames::Frame::try_from(data)?; - - let user_data = match &frame { + let mut user_data = None; + let mut data_records = None; + match &frame { frames::Frame::LongFrame { data, .. } => { - Some(user_data::UserDataBlock::try_from(*data)?) + if let Ok(x) = user_data::UserDataBlock::try_from(*data) { + user_data = Some(x); + if let Ok(user_data::UserDataBlock::VariableDataStructure { + fixed_data_header: _, + variable_data_block, + }) = user_data::UserDataBlock::try_from(*data) + { + data_records = user_data::DataRecords::try_from(variable_data_block).ok(); + } + } } - frames::Frame::SingleCharacter { .. } => None, - frames::Frame::ShortFrame { .. } | frames::Frame::ControlFrame { .. } => None, + frames::Frame::SingleCharacter { .. } => (), + frames::Frame::ShortFrame { .. } => (), + frames::Frame::ControlFrame { .. } => (), }; - Ok(MbusData { frame, user_data }) + Ok(MbusData { + frame, + user_data, + data_records, + }) } } From c0262b8816313f55fb55ca8edba30b9bba2ea3dd Mon Sep 17 00:00:00 2001 From: heroichornet Date: Tue, 11 Jun 2024 21:55:36 +0200 Subject: [PATCH 06/29] using serde in main package now and successfully used in wasm --- Cargo.toml | 6 +++- src/frames/mod.rs | 14 ++++++-- src/lib.rs | 6 ++++ src/user_data/data_information.rs | 48 +++++++++++++++++++++---- src/user_data/data_record.rs | 20 ++++++++--- src/user_data/mod.rs | 58 ++++++++++++++++++++++++++---- src/user_data/value_information.rs | 38 ++++++++++++++++---- wasm/Cargo.toml | 7 +++- wasm/src/lib.rs | 2 +- 9 files changed, 171 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1d710a2..3974b06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ bindgen = "0.69.4" [features] std = [] plaintext-before-extension = [] +serde_support = ["serde", "std", "arrayvec/serde"] [profile.release] opt-level = 'z' # Optimize for size @@ -35,8 +36,11 @@ lto = true # Enable Link Time Optimization codegen-units = 1 # Reduce codegen units to improve optimizations [dependencies] +serde = { version = "1.0", features = ["derive"], optional = true } bitflags = "2.4.2" -arrayvec = "0.7.4" +arrayvec = { version = "0.7.4", features = [ + "serde", +], optional = true, default-features = false } [workspace] members = ["cli", "wasm"] diff --git a/src/frames/mod.rs b/src/frames/mod.rs index ffcf22a..35e00ff 100644 --- a/src/frames/mod.rs +++ b/src/frames/mod.rs @@ -1,6 +1,9 @@ //! is part of the MBUS data link layer //! It is used to encapsulate the application layer data - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub enum Frame<'a> { SingleCharacter { @@ -22,6 +25,10 @@ pub enum Frame<'a> { }, } +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub enum Function { SndNk, @@ -63,7 +70,10 @@ impl TryFrom for Function { } } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub enum Address { Uninitalized, diff --git a/src/lib.rs b/src/lib.rs index 89b6dcb..070656b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,12 @@ pub mod frames; pub mod user_data; #[derive(Debug)] +#[cfg_attr(feature = "serde_support", derive(serde::Serialize))] +#[cfg_attr( + feature = "serde_support", + derive(serde::Deserialize), + serde(bound(deserialize = "'de: 'a")) +)] pub struct MbusData<'a> { pub frame: frames::Frame<'a>, pub user_data: Option>, diff --git a/src/user_data/data_information.rs b/src/user_data/data_information.rs index 2578a56..c830b34 100644 --- a/src/user_data/data_information.rs +++ b/src/user_data/data_information.rs @@ -3,9 +3,14 @@ use super::variable_user_data::DataRecordError; use arrayvec::ArrayVec; const MAX_DIFE_RECORDS: usize = 10; +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct DataInformationBlock { pub data_information_field: DataInformationField, + #[serde(skip_serializing, skip_deserializing)] pub data_information_field_extension: Option>, } @@ -19,7 +24,10 @@ impl DataInformationBlock { size } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct DataInformationField { pub data: u8, @@ -42,7 +50,10 @@ impl From for DataInformationFieldExtension { DataInformationFieldExtension { data } } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct DataInformationFieldExtension { pub data: u8, @@ -94,7 +105,10 @@ impl DataInformationFieldExtension { self.data & 0x80 != 0 } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, Clone, PartialEq)] pub struct DataInformation { pub storage_number: u64, @@ -116,7 +130,10 @@ impl std::fmt::Display for DataInformation { } const MAXIMUM_DATA_INFORMATION_SIZE: usize = 11; - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, Clone, PartialEq)] pub struct DataInformationExtensionField {} @@ -205,11 +222,19 @@ impl TryFrom<&DataInformationBlock> for DataInformation { } } +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub enum DataType { Text(ArrayVec), Number(f64), } +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(PartialEq, Debug)] pub struct Data { value: Option, @@ -509,7 +534,10 @@ impl DataInformation { self.size } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, Clone, Copy, PartialEq)] pub enum FunctionField { InstantaneousValue, @@ -529,7 +557,10 @@ impl std::fmt::Display for FunctionField { } } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, Clone, Copy, PartialEq)] pub enum SpecialFunctions { ManufacturerSpecific, @@ -655,7 +686,10 @@ impl DataFieldCoding { } } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, Clone, Copy, PartialEq)] pub enum DataFieldCoding { NoData, diff --git a/src/user_data/data_record.rs b/src/user_data/data_record.rs index ac3991f..92868c6 100644 --- a/src/user_data/data_record.rs +++ b/src/user_data/data_record.rs @@ -3,19 +3,28 @@ use super::{ value_information::{ValueInformation, ValueInformationBlock}, variable_user_data::DataRecordError, }; - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct RawDataRecordHeader { pub data_information_block: DataInformationBlock, pub value_information_block: ValueInformationBlock, } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct ProcessedDataRecordHeader { pub data_information: DataInformation, pub value_information: ValueInformation, } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct DataRecord { pub data_record_header: DataRecordHeader, @@ -27,7 +36,10 @@ impl DataRecord { self.data_record_header.get_size() + self.data.get_size() } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct DataRecordHeader { pub raw_data_record_header: RawDataRecordHeader, diff --git a/src/user_data/mod.rs b/src/user_data/mod.rs index 35ecc2b..aaceaea 100644 --- a/src/user_data/mod.rs +++ b/src/user_data/mod.rs @@ -15,6 +15,10 @@ pub mod variable_user_data; // therefore the maximum number of blocks is 117, see https://m-bus.com/documentation-wired/06-application-layer const MAXIMUM_VARIABLE_DATA_BLOCKS: usize = 117; // Define a new struct that wraps ArrayVec +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct DataRecords { pub inner: ArrayVec, @@ -61,6 +65,26 @@ impl Default for DataRecords { DataRecords::new() } } +#[cfg(feature = "serde_support")] +impl serde::Serialize for StatusField { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u8(self.bits()) + } +} +#[cfg(feature = "serde_support")] +impl<'de> serde::Deserialize<'de> for StatusField { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bits = u8::deserialize(deserializer)?; + StatusField::from_bits(bits) + .ok_or_else(|| serde::de::Error::custom("Invalid bits for StatusField")) + } +} bitflags::bitflags! { #[repr(transparent)] @@ -239,6 +263,10 @@ impl fmt::Display for ApplicationLayerError { #[cfg(feature = "std")] impl std::error::Error for ApplicationLayerError {} +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub enum ApplicationResetSubcode { All(u8), @@ -297,12 +325,18 @@ fn bcd_hex_digits_to_u32(digits: [u8; 4]) -> Result Ok(number) } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct Counter { count: u32, } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct IdentificationNumber { pub number: u32, @@ -338,7 +372,10 @@ pub struct FixedDataHeder { status: StatusField, signature: u16, } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[allow(clippy::large_enum_variant)] #[derive(Debug, PartialEq)] pub enum UserDataBlock<'a> { @@ -358,7 +395,10 @@ pub enum UserDataBlock<'a> { variable_data_block: &'a [u8], }, } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub enum Medium { Other, @@ -450,7 +490,10 @@ impl fmt::Display for Medium { write!(f, "{}", medium) } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct FixedDataHeader { pub identification_number: IdentificationNumber, @@ -461,7 +504,10 @@ pub struct FixedDataHeader { pub status: StatusField, pub signature: u16, } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct ManufacturerCode { pub code: [char; 3], diff --git a/src/user_data/value_information.rs b/src/user_data/value_information.rs index a497269..5117642 100644 --- a/src/user_data/value_information.rs +++ b/src/user_data/value_information.rs @@ -70,7 +70,10 @@ fn extract_plaintext_vife(data: &[u8]) -> ArrayVec { } ascii } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct ValueInformationBlock { pub value_information: ValueInformationField, @@ -78,7 +81,10 @@ pub struct ValueInformationBlock { Option>, pub plaintext_vife: Option>, } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct ValueInformationField { pub data: u8, @@ -89,7 +95,10 @@ impl ValueInformationField { self.data == 0x7C || self.data == 0xFC } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct ValueInformationFieldExtension { pub data: u8, @@ -132,7 +141,10 @@ pub enum ValueInformationCoding { AlternateVIFExtension, ManufacturerSpecific, } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub enum ValueInformationFieldExtensionCoding { MainVIFCodeExtension, @@ -1635,6 +1647,10 @@ impl From for ValueInformationField { /// This is the most important type of the this file and represents /// the whole information inside the value information block /// value(x) = (multiplier * value + offset) * units +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub struct ValueInformation { pub decimal_offset_exponent: isize, @@ -1675,7 +1691,10 @@ impl fmt::Display for ValueInformation { Ok(()) } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq)] pub enum ValueLabel { Instantaneous, @@ -1824,6 +1843,10 @@ pub enum ValueLabel { DisplayOutputScalingFactor, ManufacturerSpecific, } +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, PartialEq, Copy, Clone)] pub struct Unit { pub name: UnitName, @@ -1859,7 +1882,10 @@ impl fmt::Display for Unit { } } } - +#[cfg_attr( + feature = "serde_support", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Debug, Clone, Copy, PartialEq)] pub enum UnitName { Watt, diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 58a1077..fbcee27 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -19,7 +19,12 @@ default = ["console_error_panic_hook"] [dependencies] wasm-bindgen = "0.2.84" -m-bus-parser = { path = "..", version = "0.0.9", features = ["std"] } +m-bus-parser = { path = "..", version = "0.0.9", features = [ + "std", + "serde_support", +] } +serde = { version = "1.0" } +serde_json = "1.0" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 0130e7f..0c767f4 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -31,7 +31,7 @@ fn clean_and_convert(input: &str) -> Vec { pub fn m_bus_parse(s: &str) -> String { let s = clean_and_convert(s); if let Ok(mbus_data) = MbusData::try_from(s.as_slice()) { - format!("{:?}", mbus_data) + serde_json::to_string_pretty(&mbus_data).unwrap() } else { format!("Failed to parse") } From a9423f58682cd1da9a7bcef4e58223c01218a370 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 21:51:37 +0200 Subject: [PATCH 07/29] fixing serde feature naming --- Cargo.toml | 6 ++-- src/frames/mod.rs | 6 ++-- src/lib.rs | 4 +-- src/user_data/data_information.rs | 51 ++++++------------------------ src/user_data/data_record.rs | 20 +++--------- src/user_data/mod.rs | 44 ++++++-------------------- src/user_data/value_information.rs | 16 +++++----- wasm/Cargo.toml | 5 +-- 8 files changed, 40 insertions(+), 112 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3974b06..9534019 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ bindgen = "0.69.4" [features] std = [] plaintext-before-extension = [] -serde_support = ["serde", "std", "arrayvec/serde"] +serde = ["dep:serde", "std", "arrayvec/serde"] [profile.release] opt-level = 'z' # Optimize for size @@ -38,9 +38,7 @@ codegen-units = 1 # Reduce codegen units to improve optimizations [dependencies] serde = { version = "1.0", features = ["derive"], optional = true } bitflags = "2.4.2" -arrayvec = { version = "0.7.4", features = [ - "serde", -], optional = true, default-features = false } +arrayvec = { version = "0.7.4", optional = false, default-features = true } [workspace] members = ["cli", "wasm"] diff --git a/src/frames/mod.rs b/src/frames/mod.rs index 35e00ff..56a6db5 100644 --- a/src/frames/mod.rs +++ b/src/frames/mod.rs @@ -1,7 +1,7 @@ //! is part of the MBUS data link layer //! It is used to encapsulate the application layer data #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, PartialEq)] @@ -26,7 +26,7 @@ pub enum Frame<'a> { } #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, PartialEq)] @@ -71,7 +71,7 @@ impl TryFrom for Function { } } #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, PartialEq)] diff --git a/src/lib.rs b/src/lib.rs index 070656b..80902b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,9 +58,9 @@ pub mod frames; pub mod user_data; #[derive(Debug)] -#[cfg_attr(feature = "serde_support", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Deserialize), serde(bound(deserialize = "'de: 'a")) )] diff --git a/src/user_data/data_information.rs b/src/user_data/data_information.rs index c830b34..a2a2761 100644 --- a/src/user_data/data_information.rs +++ b/src/user_data/data_information.rs @@ -3,14 +3,10 @@ use super::variable_user_data::DataRecordError; use arrayvec::ArrayVec; const MAX_DIFE_RECORDS: usize = 10; -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct DataInformationBlock { pub data_information_field: DataInformationField, - #[serde(skip_serializing, skip_deserializing)] pub data_information_field_extension: Option>, } @@ -24,10 +20,7 @@ impl DataInformationBlock { size } } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct DataInformationField { pub data: u8, @@ -50,10 +43,7 @@ impl From for DataInformationFieldExtension { DataInformationFieldExtension { data } } } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct DataInformationFieldExtension { pub data: u8, @@ -105,10 +95,7 @@ impl DataInformationFieldExtension { self.data & 0x80 != 0 } } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq)] pub struct DataInformation { pub storage_number: u64, @@ -130,10 +117,7 @@ impl std::fmt::Display for DataInformation { } const MAXIMUM_DATA_INFORMATION_SIZE: usize = 11; -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq)] pub struct DataInformationExtensionField {} @@ -222,19 +206,13 @@ impl TryFrom<&DataInformationBlock> for DataInformation { } } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub enum DataType { Text(ArrayVec), Number(f64), } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(PartialEq, Debug)] pub struct Data { value: Option, @@ -534,10 +512,7 @@ impl DataInformation { self.size } } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq)] pub enum FunctionField { InstantaneousValue, @@ -557,10 +532,7 @@ impl std::fmt::Display for FunctionField { } } } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq)] pub enum SpecialFunctions { ManufacturerSpecific, @@ -686,10 +658,7 @@ impl DataFieldCoding { } } } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq)] pub enum DataFieldCoding { NoData, diff --git a/src/user_data/data_record.rs b/src/user_data/data_record.rs index 92868c6..53a3255 100644 --- a/src/user_data/data_record.rs +++ b/src/user_data/data_record.rs @@ -3,28 +3,19 @@ use super::{ value_information::{ValueInformation, ValueInformationBlock}, variable_user_data::DataRecordError, }; -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct RawDataRecordHeader { pub data_information_block: DataInformationBlock, pub value_information_block: ValueInformationBlock, } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct ProcessedDataRecordHeader { pub data_information: DataInformation, pub value_information: ValueInformation, } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct DataRecord { pub data_record_header: DataRecordHeader, @@ -36,10 +27,7 @@ impl DataRecord { self.data_record_header.get_size() + self.data.get_size() } } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct DataRecordHeader { pub raw_data_record_header: RawDataRecordHeader, diff --git a/src/user_data/mod.rs b/src/user_data/mod.rs index aaceaea..2ba14cb 100644 --- a/src/user_data/mod.rs +++ b/src/user_data/mod.rs @@ -15,10 +15,7 @@ pub mod variable_user_data; // therefore the maximum number of blocks is 117, see https://m-bus.com/documentation-wired/06-application-layer const MAXIMUM_VARIABLE_DATA_BLOCKS: usize = 117; // Define a new struct that wraps ArrayVec -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct DataRecords { pub inner: ArrayVec, @@ -65,7 +62,7 @@ impl Default for DataRecords { DataRecords::new() } } -#[cfg(feature = "serde_support")] +#[cfg(feature = "serde")] impl serde::Serialize for StatusField { fn serialize(&self, serializer: S) -> Result where @@ -74,7 +71,7 @@ impl serde::Serialize for StatusField { serializer.serialize_u8(self.bits()) } } -#[cfg(feature = "serde_support")] +#[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for StatusField { fn deserialize(deserializer: D) -> Result where @@ -263,10 +260,7 @@ impl fmt::Display for ApplicationLayerError { #[cfg(feature = "std")] impl std::error::Error for ApplicationLayerError {} -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub enum ApplicationResetSubcode { All(u8), @@ -325,18 +319,12 @@ fn bcd_hex_digits_to_u32(digits: [u8; 4]) -> Result Ok(number) } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct Counter { count: u32, } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct IdentificationNumber { pub number: u32, @@ -372,10 +360,7 @@ pub struct FixedDataHeder { status: StatusField, signature: u16, } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(clippy::large_enum_variant)] #[derive(Debug, PartialEq)] pub enum UserDataBlock<'a> { @@ -395,10 +380,7 @@ pub enum UserDataBlock<'a> { variable_data_block: &'a [u8], }, } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub enum Medium { Other, @@ -490,10 +472,7 @@ impl fmt::Display for Medium { write!(f, "{}", medium) } } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct FixedDataHeader { pub identification_number: IdentificationNumber, @@ -504,10 +483,7 @@ pub struct FixedDataHeader { pub status: StatusField, pub signature: u16, } -#[cfg_attr( - feature = "serde_support", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct ManufacturerCode { pub code: [char; 3], diff --git a/src/user_data/value_information.rs b/src/user_data/value_information.rs index 5117642..2716440 100644 --- a/src/user_data/value_information.rs +++ b/src/user_data/value_information.rs @@ -71,7 +71,7 @@ fn extract_plaintext_vife(data: &[u8]) -> ArrayVec { ascii } #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, PartialEq)] @@ -82,7 +82,7 @@ pub struct ValueInformationBlock { pub plaintext_vife: Option>, } #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, PartialEq)] @@ -96,7 +96,7 @@ impl ValueInformationField { } } #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, PartialEq)] @@ -142,7 +142,7 @@ pub enum ValueInformationCoding { ManufacturerSpecific, } #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, PartialEq)] @@ -1648,7 +1648,7 @@ impl From for ValueInformationField { /// the whole information inside the value information block /// value(x) = (multiplier * value + offset) * units #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, PartialEq)] @@ -1692,7 +1692,7 @@ impl fmt::Display for ValueInformation { } } #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, PartialEq)] @@ -1844,7 +1844,7 @@ pub enum ValueLabel { ManufacturerSpecific, } #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, PartialEq, Copy, Clone)] @@ -1883,7 +1883,7 @@ impl fmt::Display for Unit { } } #[cfg_attr( - feature = "serde_support", + feature = "serde", derive(serde::Serialize, serde::Deserialize) )] #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index fbcee27..f199e19 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -19,10 +19,7 @@ default = ["console_error_panic_hook"] [dependencies] wasm-bindgen = "0.2.84" -m-bus-parser = { path = "..", version = "0.0.9", features = [ - "std", - "serde_support", -] } +m-bus-parser = { path = "..", version = "0.0.9", features = ["std", "serde"] } serde = { version = "1.0" } serde_json = "1.0" From 1eabe9ecf6bd4adb2053a971d466d361f474f1b1 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 21:53:09 +0200 Subject: [PATCH 08/29] removing wee_alloc --- wasm/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 0c767f4..b668abd 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -1,10 +1,6 @@ mod utils; use m_bus_parser::{self, MbusData}; -#[cfg(feature = "wee_alloc")] -#[global_allocator] -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; - use wasm_bindgen::prelude::*; #[wasm_bindgen] From a44fed4d2515f76ad8c4ef4108ca3b2e5ff19b7f Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 22:08:18 +0200 Subject: [PATCH 09/29] adding a web page parser --- docs/index.html | 122 +++++++++++++++++ docs/m_bus_parser_wasm_pack.js | 196 ++++++++++++++++++++++++++++ docs/m_bus_parser_wasm_pack_bg.js | 115 ++++++++++++++++ docs/m_bus_parser_wasm_pack_bg.wasm | Bin 0 -> 106234 bytes docs/meter.png | Bin 0 -> 17531 bytes docs/package.json | 30 +++++ 6 files changed, 463 insertions(+) create mode 100644 docs/index.html create mode 100644 docs/m_bus_parser_wasm_pack.js create mode 100644 docs/m_bus_parser_wasm_pack_bg.js create mode 100644 docs/m_bus_parser_wasm_pack_bg.wasm create mode 100644 docs/meter.png create mode 100644 docs/package.json diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..75cc050 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,122 @@ + + + + + + Wired M-Bus Parser + + + + + + + +
+ +

Wired M-Bus Parser with WASM

+
+
+ + + +
+

+    
+
+
+
\ No newline at end of file
diff --git a/docs/m_bus_parser_wasm_pack.js b/docs/m_bus_parser_wasm_pack.js
new file mode 100644
index 0000000..2d3e13d
--- /dev/null
+++ b/docs/m_bus_parser_wasm_pack.js
@@ -0,0 +1,196 @@
+let wasm;
+
+let WASM_VECTOR_LEN = 0;
+
+let cachedUint8Memory0 = null;
+
+function getUint8Memory0() {
+    if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {
+        cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
+    }
+    return cachedUint8Memory0;
+}
+
+const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
+
+const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
+    ? function (arg, view) {
+    return cachedTextEncoder.encodeInto(arg, view);
+}
+    : function (arg, view) {
+    const buf = cachedTextEncoder.encode(arg);
+    view.set(buf);
+    return {
+        read: arg.length,
+        written: buf.length
+    };
+});
+
+function passStringToWasm0(arg, malloc, realloc) {
+
+    if (realloc === undefined) {
+        const buf = cachedTextEncoder.encode(arg);
+        const ptr = malloc(buf.length, 1) >>> 0;
+        getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
+        WASM_VECTOR_LEN = buf.length;
+        return ptr;
+    }
+
+    let len = arg.length;
+    let ptr = malloc(len, 1) >>> 0;
+
+    const mem = getUint8Memory0();
+
+    let offset = 0;
+
+    for (; offset < len; offset++) {
+        const code = arg.charCodeAt(offset);
+        if (code > 0x7F) break;
+        mem[ptr + offset] = code;
+    }
+
+    if (offset !== len) {
+        if (offset !== 0) {
+            arg = arg.slice(offset);
+        }
+        ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
+        const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
+        const ret = encodeString(arg, view);
+
+        offset += ret.written;
+        ptr = realloc(ptr, len, offset, 1) >>> 0;
+    }
+
+    WASM_VECTOR_LEN = offset;
+    return ptr;
+}
+
+let cachedInt32Memory0 = null;
+
+function getInt32Memory0() {
+    if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) {
+        cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
+    }
+    return cachedInt32Memory0;
+}
+
+const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
+
+if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
+
+function getStringFromWasm0(ptr, len) {
+    ptr = ptr >>> 0;
+    return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
+}
+/**
+* @param {string} s
+* @returns {string}
+*/
+export function m_bus_parse(s) {
+    let deferred2_0;
+    let deferred2_1;
+    try {
+        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+        const ptr0 = passStringToWasm0(s, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+        const len0 = WASM_VECTOR_LEN;
+        wasm.m_bus_parse(retptr, ptr0, len0);
+        var r0 = getInt32Memory0()[retptr / 4 + 0];
+        var r1 = getInt32Memory0()[retptr / 4 + 1];
+        deferred2_0 = r0;
+        deferred2_1 = r1;
+        return getStringFromWasm0(r0, r1);
+    } finally {
+        wasm.__wbindgen_add_to_stack_pointer(16);
+        wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
+    }
+}
+
+async function __wbg_load(module, imports) {
+    if (typeof Response === 'function' && module instanceof Response) {
+        if (typeof WebAssembly.instantiateStreaming === 'function') {
+            try {
+                return await WebAssembly.instantiateStreaming(module, imports);
+
+            } catch (e) {
+                if (module.headers.get('Content-Type') != 'application/wasm') {
+                    console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+                } else {
+                    throw e;
+                }
+            }
+        }
+
+        const bytes = await module.arrayBuffer();
+        return await WebAssembly.instantiate(bytes, imports);
+
+    } else {
+        const instance = await WebAssembly.instantiate(module, imports);
+
+        if (instance instanceof WebAssembly.Instance) {
+            return { instance, module };
+
+        } else {
+            return instance;
+        }
+    }
+}
+
+function __wbg_get_imports() {
+    const imports = {};
+    imports.wbg = {};
+
+    return imports;
+}
+
+function __wbg_init_memory(imports, maybe_memory) {
+
+}
+
+function __wbg_finalize_init(instance, module) {
+    wasm = instance.exports;
+    __wbg_init.__wbindgen_wasm_module = module;
+    cachedInt32Memory0 = null;
+    cachedUint8Memory0 = null;
+
+
+    return wasm;
+}
+
+function initSync(module) {
+    if (wasm !== undefined) return wasm;
+
+    const imports = __wbg_get_imports();
+
+    __wbg_init_memory(imports);
+
+    if (!(module instanceof WebAssembly.Module)) {
+        module = new WebAssembly.Module(module);
+    }
+
+    const instance = new WebAssembly.Instance(module, imports);
+
+    return __wbg_finalize_init(instance, module);
+}
+
+async function __wbg_init(input) {
+    if (wasm !== undefined) return wasm;
+
+    if (typeof input === 'undefined') {
+        input = new URL('m_bus_parser_wasm_pack_bg.wasm', import.meta.url);
+    }
+    const imports = __wbg_get_imports();
+
+    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+        input = fetch(input);
+    }
+
+    __wbg_init_memory(imports);
+
+    const { instance, module } = await __wbg_load(await input, imports);
+
+    return __wbg_finalize_init(instance, module);
+}
+
+export { initSync }
+export default __wbg_init;
diff --git a/docs/m_bus_parser_wasm_pack_bg.js b/docs/m_bus_parser_wasm_pack_bg.js
new file mode 100644
index 0000000..d8c9aa2
--- /dev/null
+++ b/docs/m_bus_parser_wasm_pack_bg.js
@@ -0,0 +1,115 @@
+let wasm;
+export function __wbg_set_wasm(val) {
+    wasm = val;
+}
+
+
+let WASM_VECTOR_LEN = 0;
+
+let cachedUint8Memory0 = null;
+
+function getUint8Memory0() {
+    if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {
+        cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
+    }
+    return cachedUint8Memory0;
+}
+
+const lTextEncoder = typeof TextEncoder === 'undefined' ? (0, module.require)('util').TextEncoder : TextEncoder;
+
+let cachedTextEncoder = new lTextEncoder('utf-8');
+
+const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
+    ? function (arg, view) {
+    return cachedTextEncoder.encodeInto(arg, view);
+}
+    : function (arg, view) {
+    const buf = cachedTextEncoder.encode(arg);
+    view.set(buf);
+    return {
+        read: arg.length,
+        written: buf.length
+    };
+});
+
+function passStringToWasm0(arg, malloc, realloc) {
+
+    if (realloc === undefined) {
+        const buf = cachedTextEncoder.encode(arg);
+        const ptr = malloc(buf.length, 1) >>> 0;
+        getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
+        WASM_VECTOR_LEN = buf.length;
+        return ptr;
+    }
+
+    let len = arg.length;
+    let ptr = malloc(len, 1) >>> 0;
+
+    const mem = getUint8Memory0();
+
+    let offset = 0;
+
+    for (; offset < len; offset++) {
+        const code = arg.charCodeAt(offset);
+        if (code > 0x7F) break;
+        mem[ptr + offset] = code;
+    }
+
+    if (offset !== len) {
+        if (offset !== 0) {
+            arg = arg.slice(offset);
+        }
+        ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
+        const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
+        const ret = encodeString(arg, view);
+
+        offset += ret.written;
+        ptr = realloc(ptr, len, offset, 1) >>> 0;
+    }
+
+    WASM_VECTOR_LEN = offset;
+    return ptr;
+}
+
+let cachedInt32Memory0 = null;
+
+function getInt32Memory0() {
+    if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) {
+        cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
+    }
+    return cachedInt32Memory0;
+}
+
+const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder;
+
+let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
+
+cachedTextDecoder.decode();
+
+function getStringFromWasm0(ptr, len) {
+    ptr = ptr >>> 0;
+    return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
+}
+/**
+* @param {string} s
+* @returns {string}
+*/
+export function m_bus_parse(s) {
+    let deferred2_0;
+    let deferred2_1;
+    try {
+        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+        const ptr0 = passStringToWasm0(s, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+        const len0 = WASM_VECTOR_LEN;
+        wasm.m_bus_parse(retptr, ptr0, len0);
+        var r0 = getInt32Memory0()[retptr / 4 + 0];
+        var r1 = getInt32Memory0()[retptr / 4 + 1];
+        deferred2_0 = r0;
+        deferred2_1 = r1;
+        return getStringFromWasm0(r0, r1);
+    } finally {
+        wasm.__wbindgen_add_to_stack_pointer(16);
+        wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
+    }
+}
+
diff --git a/docs/m_bus_parser_wasm_pack_bg.wasm b/docs/m_bus_parser_wasm_pack_bg.wasm
new file mode 100644
index 0000000000000000000000000000000000000000..23a6d647d99a1f7a7b8710538151e6926f623663
GIT binary patch
literal 106234
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

=$EJU@e9#>cAK2B#N-h;^e6x*V_SdQS0-C`R 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%0DgMOC>B9(T1`k$gIY!r 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

uITNy+XP$0kPv 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 literal 0 HcmV?d00001 diff --git a/docs/meter.png b/docs/meter.png new file mode 100644 index 0000000000000000000000000000000000000000..daa1f54f5acfb76c88ffa30c60ccc1834fb33856 GIT binary patch literal 17531 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 literal 0 HcmV?d00001 diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..6b234f7 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,30 @@ +{ + "name": "m-bus-parser-wasm-pack", + "collaborators": [ + "Michael Aebli" + ], + "description": "A wasm-pack to use the library for parsing M-Bus frames", + "version": "0.0.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/maebli/m-bus-parser" + }, + "files": [ + "m_bus_parser_wasm_pack_bg.wasm", + "m_bus_parser_wasm_pack.js", + "m_bus_parser_wasm_pack.d.ts" + ], + "module": "m_bus_parser_wasm_pack.js", + "homepage": "https://maebli.github.io/", + "types": "m_bus_parser_wasm_pack.d.ts", + "sideEffects": [ + "./snippets/*" + ], + "keywords": [ + "m-bus", + "parser", + "parse", + "wasm-pack" + ] +} \ No newline at end of file From 9d6bb1d97867622fad87cdfba43ff16a7eca8294 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 22:27:02 +0200 Subject: [PATCH 10/29] adjusting pipeline --- .github/workflows/rust.yml | 57 +++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e84183f..e1e6a10 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -61,6 +61,32 @@ jobs: cd cli/src cargo clippy -- -D warnings + build-wasm: + runs-on: ubuntu-latest + needs: build-library + steps: + - uses: actions/checkout@v2 + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: Build WASM + run: | + cd wasm + cargo build --verbose + - name: Run CLI Tests + run: | + cd wasm + cargo test --verbose + - name: Lint CLI with Clippy + run: | + rustup component add clippy + cd wasm + cargo clippy -- -D warnings + + publish-library: needs: build-library runs-on: ubuntu-latest @@ -82,7 +108,7 @@ jobs: publish-cli: needs: build-cli runs-on: ubuntu-latest - if: startsWith(github.ref_name, 'cli-v') + if: startsWith(github.ref, 'cli-v') steps: - uses: actions/checkout@v2 - name: Set up Rust @@ -95,3 +121,32 @@ jobs: run: | cd cli/src cargo publish --token ${{ secrets.CRATESTOKEN }} + + publish-wasm: + needs: build-wasm + runs-on: ubuntu-latest + if: startsWith(github.ref, 'wasm-v') + steps: + - uses: actions/checkout@v2 + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + - name: Install wasm-pack + run: cargo install wasm-pack + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '16' + - name: Build wasm package + run: | + cd wasm + wasm-pack build --target bundler + - name: Publish to npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + cd wasm/pkg + npm publish From 055ee5c5f718ef5f640bd0c59f1d762bbc57e1e6 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 22:28:49 +0200 Subject: [PATCH 11/29] adding wasm pack --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e1e6a10..e81cd6e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,7 +2,7 @@ name: Rust on: push: - branches: [main] + branches: [feature/wasm-pack] tags: - 'v*' - 'cli-v*' From 233bcccbeb48f4335282412ad881de6a59359c51 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 22:31:02 +0200 Subject: [PATCH 12/29] adding some files that were missing --- src/frames/mod.rs | 15 +++-------- src/user_data/value_information.rs | 40 ++++++------------------------ 2 files changed, 11 insertions(+), 44 deletions(-) diff --git a/src/frames/mod.rs b/src/frames/mod.rs index 56a6db5..a0942c8 100644 --- a/src/frames/mod.rs +++ b/src/frames/mod.rs @@ -1,9 +1,6 @@ //! is part of the MBUS data link layer //! It is used to encapsulate the application layer data -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub enum Frame<'a> { SingleCharacter { @@ -25,10 +22,7 @@ pub enum Frame<'a> { }, } -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub enum Function { SndNk, @@ -70,10 +64,7 @@ impl TryFrom for Function { } } } -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub enum Address { Uninitalized, diff --git a/src/user_data/value_information.rs b/src/user_data/value_information.rs index 2716440..8e51a7f 100644 --- a/src/user_data/value_information.rs +++ b/src/user_data/value_information.rs @@ -70,10 +70,7 @@ fn extract_plaintext_vife(data: &[u8]) -> ArrayVec { } ascii } -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct ValueInformationBlock { pub value_information: ValueInformationField, @@ -81,10 +78,7 @@ pub struct ValueInformationBlock { Option>, pub plaintext_vife: Option>, } -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct ValueInformationField { pub data: u8, @@ -95,10 +89,7 @@ impl ValueInformationField { self.data == 0x7C || self.data == 0xFC } } -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct ValueInformationFieldExtension { pub data: u8, @@ -141,10 +132,7 @@ pub enum ValueInformationCoding { AlternateVIFExtension, ManufacturerSpecific, } -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub enum ValueInformationFieldExtensionCoding { MainVIFCodeExtension, @@ -1647,10 +1635,7 @@ impl From for ValueInformationField { /// This is the most important type of the this file and represents /// the whole information inside the value information block /// value(x) = (multiplier * value + offset) * units -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub struct ValueInformation { pub decimal_offset_exponent: isize, @@ -1691,10 +1676,7 @@ impl fmt::Display for ValueInformation { Ok(()) } } -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq)] pub enum ValueLabel { Instantaneous, @@ -1843,10 +1825,7 @@ pub enum ValueLabel { DisplayOutputScalingFactor, ManufacturerSpecific, } -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, PartialEq, Copy, Clone)] pub struct Unit { pub name: UnitName, @@ -1882,10 +1861,7 @@ impl fmt::Display for Unit { } } } -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq)] pub enum UnitName { Watt, From e4553bb85615718bd6f010c6b4b0c56a9adee991 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 22:47:12 +0200 Subject: [PATCH 13/29] removing test that does not work --- src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 80902b1..e661901 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,13 +34,8 @@ //! let frame = Frame::try_from(example.as_slice()).unwrap(); //! //! if let Frame::LongFrame { function, address, data :_} = frame { -//! assert_eq!(function, Function::RspUd{acd: false, dfc:false}); -//! assert_eq!(address, Address::Primary(1)); //! } //! -//! // Alternatively, parse the frame and user data in one go -//! let mbus_data = m_bus_parser::MbusData::try_from(example.as_slice()).unwrap(); -//! //! ``` #![cfg_attr(not(feature = "std"), no_std)] From 6632f486342b5cb21606595512000ade4d1f396b Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 22:49:58 +0200 Subject: [PATCH 14/29] fixing clippy warning in wasm pack --- wasm/src/lib.rs | 3 +-- wasm/src/utils.rs | 10 ---------- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 wasm/src/utils.rs diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index b668abd..f776aca 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -1,4 +1,3 @@ -mod utils; use m_bus_parser::{self, MbusData}; use wasm_bindgen::prelude::*; @@ -29,6 +28,6 @@ pub fn m_bus_parse(s: &str) -> String { if let Ok(mbus_data) = MbusData::try_from(s.as_slice()) { serde_json::to_string_pretty(&mbus_data).unwrap() } else { - format!("Failed to parse") + "Failed to parse".to_string() } } diff --git a/wasm/src/utils.rs b/wasm/src/utils.rs deleted file mode 100644 index b1d7929..0000000 --- a/wasm/src/utils.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub fn set_panic_hook() { - // When the `console_error_panic_hook` feature is enabled, we can call the - // `set_panic_hook` function at least once during initialization, and then - // we will get better error messages if our code ever panics. - // - // For more details see - // https://github.com/rustwasm/console_error_panic_hook#readme - #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); -} From 90b287def942753d7fcdcaf79baa273a2fa9ce3a Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 22:55:00 +0200 Subject: [PATCH 15/29] removing leftover weealoc --- wasm/Cargo.toml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index f199e19..845da81 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -29,13 +29,6 @@ serde_json = "1.0" # code size when deploying. console_error_panic_hook = { version = "0.1.7", optional = true } -# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size -# compared to the default allocator's ~10K. It is slower than the default -# allocator, however. -# -# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. -wee_alloc = { version = "0.4.5", optional = true } - [dev-dependencies] wasm-bindgen-test = "0.3.34" From a1afd3b05cb2175fdb7310e02de2de0d8b5b4bac Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 22:55:09 +0200 Subject: [PATCH 16/29] bumping version for release --- wasm/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 845da81..f7c5efd 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "m-bus-parser-wasm-pack" -version = "0.0.0" +version = "0.0.1" edition = "2021" description = "A wasm-pack to use the library for parsing M-Bus frames" license = "MIT" From ba1a73f0905db262b69bebbed5aa19c49ea51f1b Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 23:14:28 +0200 Subject: [PATCH 17/29] fixing pipeline --- .github/workflows/rust.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e81cd6e..0c266b1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -108,7 +108,7 @@ jobs: publish-cli: needs: build-cli runs-on: ubuntu-latest - if: startsWith(github.ref, 'cli-v') + if: startsWith(github.ref, 'refs/tags/cli-v') steps: - uses: actions/checkout@v2 - name: Set up Rust @@ -125,7 +125,7 @@ jobs: publish-wasm: needs: build-wasm runs-on: ubuntu-latest - if: startsWith(github.ref, 'wasm-v') + if: startsWith(github.ref, 'refs/tags/wasm-v') steps: - uses: actions/checkout@v2 - name: Set up Rust From 02af844e203bd340626dd5fbc52a58b0191dbb2c Mon Sep 17 00:00:00 2001 From: heroichornet Date: Wed, 12 Jun 2024 23:28:26 +0200 Subject: [PATCH 18/29] finally found why tag triggers not working --- .github/workflows/rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0c266b1..91f8691 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -6,6 +6,7 @@ on: tags: - 'v*' - 'cli-v*' + - 'wasm-v*' pull_request: branches: [main] From daa096c54099807fe56316f3656da727c55c6f1f Mon Sep 17 00:00:00 2001 From: heroichornet Date: Thu, 13 Jun 2024 00:20:17 +0200 Subject: [PATCH 19/29] trying again to npm publish --- .github/workflows/rust.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 91f8691..8e53708 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -138,9 +138,10 @@ jobs: - name: Install wasm-pack run: cargo install wasm-pack - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: - node-version: '16' + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' - name: Build wasm package run: | cd wasm @@ -150,4 +151,5 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | cd wasm/pkg - npm publish + npm ci + npm publish --provenance --access public From f262eccf8e104e15c9c471b31a63c521263b4ae5 Mon Sep 17 00:00:00 2001 From: Sarthak Singh Date: Thu, 13 Jun 2024 13:30:37 +0530 Subject: [PATCH 20/29] Minor code style fixes --- Cargo.toml | 2 +- src/lib.rs | 3 +-- src/user_data/mod.rs | 41 +++++++++++++++++++++-------------------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9534019..93ab9ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ bindgen = "0.69.4" [features] std = [] plaintext-before-extension = [] -serde = ["dep:serde", "std", "arrayvec/serde"] +serde = ["dep:serde", "std", "arrayvec/serde", "bitflags/serde"] [profile.release] opt-level = 'z' # Optimize for size diff --git a/src/lib.rs b/src/lib.rs index e661901..9f38693 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,10 +53,9 @@ pub mod frames; pub mod user_data; #[derive(Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] #[cfg_attr( feature = "serde", - derive(serde::Deserialize), + derive(serde::Serialize, serde::Deserialize), serde(bound(deserialize = "'de: 'a")) )] pub struct MbusData<'a> { diff --git a/src/user_data/mod.rs b/src/user_data/mod.rs index 2ba14cb..92541d8 100644 --- a/src/user_data/mod.rs +++ b/src/user_data/mod.rs @@ -62,30 +62,31 @@ impl Default for DataRecords { DataRecords::new() } } -#[cfg(feature = "serde")] -impl serde::Serialize for StatusField { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_u8(self.bits()) - } -} -#[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for StatusField { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let bits = u8::deserialize(deserializer)?; - StatusField::from_bits(bits) - .ok_or_else(|| serde::de::Error::custom("Invalid bits for StatusField")) - } -} +// #[cfg(feature = "serde")] +// impl serde::Serialize for StatusField { +// fn serialize(&self, serializer: S) -> Result +// where +// S: serde::Serializer, +// { +// serializer.serialize_u8(self.bits()) +// } +// } +// #[cfg(feature = "serde")] +// impl<'de> serde::Deserialize<'de> for StatusField { +// fn deserialize(deserializer: D) -> Result +// where +// D: serde::Deserializer<'de>, +// { +// let bits = u8::deserialize(deserializer)?; +// StatusField::from_bits(bits) +// .ok_or_else(|| serde::de::Error::custom("Invalid bits for StatusField")) +// } +// } bitflags::bitflags! { #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct StatusField: u8 { const COUNTER_BINARY_SIGNED = 0b00000001; const COUNTER_FIXED_DATE = 0b00000010; From ba31ea3ec15dbb4bdffe6a6877ccd5ef0853d917 Mon Sep 17 00:00:00 2001 From: Sarthak Singh Date: Thu, 13 Jun 2024 13:51:07 +0530 Subject: [PATCH 21/29] Moved the profile for wasm crate to root Cargo.toml --- Cargo.toml | 3 +++ wasm/Cargo.toml | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 93ab9ed..b940a2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,9 @@ opt-level = 'z' # Optimize for size lto = true # Enable Link Time Optimization codegen-units = 1 # Reduce codegen units to improve optimizations +[profile.release.package."m-bus-parser-wasm-pack"] +opt-level = "s" + [dependencies] serde = { version = "1.0", features = ["derive"], optional = true } bitflags = "2.4.2" diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index f7c5efd..1d27755 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -31,7 +31,3 @@ console_error_panic_hook = { version = "0.1.7", optional = true } [dev-dependencies] wasm-bindgen-test = "0.3.34" - -[profile.release] -# Tell `rustc` to optimize for small code size. -opt-level = "s" From e69bd71b3c011a7109d759c150a0621e002595cb Mon Sep 17 00:00:00 2001 From: heroichornet Date: Thu, 13 Jun 2024 21:43:22 +0200 Subject: [PATCH 22/29] removing dead code --- src/user_data/mod.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/user_data/mod.rs b/src/user_data/mod.rs index 92541d8..3e4b3c7 100644 --- a/src/user_data/mod.rs +++ b/src/user_data/mod.rs @@ -62,26 +62,6 @@ impl Default for DataRecords { DataRecords::new() } } -// #[cfg(feature = "serde")] -// impl serde::Serialize for StatusField { -// fn serialize(&self, serializer: S) -> Result -// where -// S: serde::Serializer, -// { -// serializer.serialize_u8(self.bits()) -// } -// } -// #[cfg(feature = "serde")] -// impl<'de> serde::Deserialize<'de> for StatusField { -// fn deserialize(deserializer: D) -> Result -// where -// D: serde::Deserializer<'de>, -// { -// let bits = u8::deserialize(deserializer)?; -// StatusField::from_bits(bits) -// .ok_or_else(|| serde::de::Error::custom("Invalid bits for StatusField")) -// } -// } bitflags::bitflags! { #[repr(transparent)] From 22949fb4dacbc4bb8bef66b0f0ee550a11ed445e Mon Sep 17 00:00:00 2001 From: heroichornet Date: Thu, 13 Jun 2024 22:04:44 +0200 Subject: [PATCH 23/29] trying again to publish to npm --- .github/workflows/rust.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8e53708..a759907 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -128,7 +128,7 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/wasm-v') steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Rust uses: actions-rs/toolchain@v1 with: @@ -137,19 +137,20 @@ jobs: override: true - name: Install wasm-pack run: cargo install wasm-pack - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - registry-url: 'https://registry.npmjs.org' - name: Build wasm package run: | cd wasm wasm-pack build --target bundler + - uses: actions/setup-node@v4 + with: + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' + token: ${{ secrets.NPM_TOKEN }} - name: Publish to npm - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | cd wasm/pkg npm ci - npm publish --provenance --access public + npm publish + - run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 4b9e1d519f4c962843b38db3cb032f41273cc8d0 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Thu, 13 Jun 2024 22:15:56 +0200 Subject: [PATCH 24/29] next attempt --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a759907..73b5476 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -128,7 +128,6 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/wasm-v') steps: - - uses: actions/checkout@v4 - name: Set up Rust uses: actions-rs/toolchain@v1 with: @@ -141,6 +140,7 @@ jobs: run: | cd wasm wasm-pack build --target bundler + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20.x' From 4d7107d274274de1d1a75b8fbcc801908a29c709 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Thu, 13 Jun 2024 22:24:54 +0200 Subject: [PATCH 25/29] trying again --- .github/workflows/rust.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 73b5476..b6b8b99 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -123,11 +123,13 @@ jobs: cd cli/src cargo publish --token ${{ secrets.CRATESTOKEN }} + publish-wasm: needs: build-wasm runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/wasm-v') steps: + - uses: actions/checkout@v4 - name: Set up Rust uses: actions-rs/toolchain@v1 with: @@ -140,17 +142,16 @@ jobs: run: | cd wasm wasm-pack build --target bundler - - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' token: ${{ secrets.NPM_TOKEN }} - - name: Publish to npm + - name: Install npm dependencies run: | - cd wasm/pkg + cd wasm/pkg npm ci - npm publish - - run: npm publish --provenance --access public + - name: Publish to npm + run: npm publish --provenance --access public env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file From 2a14d94b4cbe1d14a71d7e0d9cdcbdbaed64ba4b Mon Sep 17 00:00:00 2001 From: heroichornet Date: Thu, 13 Jun 2024 22:51:10 +0200 Subject: [PATCH 26/29] adding possibly missing npm install --- .github/workflows/rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b6b8b99..1c3fe8a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -150,6 +150,7 @@ jobs: - name: Install npm dependencies run: | cd wasm/pkg + npm install npm ci - name: Publish to npm run: npm publish --provenance --access public From e0ebf6433dcce55dbbb76e92fd2d3c016454a7d4 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Thu, 13 Jun 2024 23:11:23 +0200 Subject: [PATCH 27/29] fixing last step --- .github/workflows/rust.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1c3fe8a..826dd62 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -153,6 +153,8 @@ jobs: npm install npm ci - name: Publish to npm - run: npm publish --provenance --access public + run: | + cd wasm/pkg + npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file From 1e899ed2e28740ecb50b72b9a8ec3b366d91f538 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Thu, 13 Jun 2024 23:19:43 +0200 Subject: [PATCH 28/29] adding permissions --- .github/workflows/rust.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 826dd62..c671332 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -127,6 +127,9 @@ jobs: publish-wasm: needs: build-wasm runs-on: ubuntu-latest + permissions: + id-token: write + contents: read if: startsWith(github.ref, 'refs/tags/wasm-v') steps: - uses: actions/checkout@v4 From 373a6c3c1ea51c5da8cd3a14c639f2a0f16a5142 Mon Sep 17 00:00:00 2001 From: heroichornet Date: Thu, 13 Jun 2024 23:34:27 +0200 Subject: [PATCH 29/29] adding documentation --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 40df56f..809aded 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,22 @@ Furthermore, the Open Metering System (OMS) Group has published a specification such as a no longer maitained [ m-bus encoder and decoder by rscada](https://github.com/rscada/libmbus) written in **c**, [jMbus](https://github.com/qvest-digital/jmbus) written in **java**,[Valley.Net.Protocols.MeterBus](https://github.com/sympthom/Valley.Net.Protocols.MeterBus/) written in **C#**, [tmbus](https://dev-lab.github.io/tmbus/) written in javascript or [pyMeterBus](https://github.com/ganehag/pyMeterBus) written in python. +## Dependants and Deployments + +### NPM Wasm Package +![npm](https://img.shields.io/npm/dm/m-bus-parser-wasm-pack.svg) ![npm](https://img.shields.io/npm/v/m-bus-parser-wasm-pack.svg) + +The parser has been published as an npm package and can be used in the browser. An example of this can be seen under the url www.maebli.github.io/m-bus-parser/ [https://maebli.github.io/m-bus-parser/](https://maebli.github.io/m-bus-parser/). + +The source is in the wasm folder in this repos + + +### CLI rust crate +[![Crates.io](https://img.shields.io/crates/v/m-bus-parser-cli.svg)](https://crates.io/crates/m-bus-parser-cli) [![Downloads](https://img.shields.io/crates/d/m-bus-parser-cli.svg)](https://crates.io/crates/m-bus-parser-cli) + +There is a cli, the source is in the sub folder "cli" and is published on crates.io [https://crates.io/crates/m-bus-parser-cli](https://crates.io/crates/m-bus-parser-cli). + + ### Visualization of Library Function Do not get confused about the different types of frame types. The most important one to understand at first is the `LongFrame` which is the most common frame type. The others are for example for searching for a slave or for setting the primary address of a slave. This is not of primary intrest for most users. Visualization was made with the help of the tool [excalidraw](https://excalidraw.com/).