diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e84183f..c671332 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,10 +2,11 @@ name: Rust on: push: - branches: [main] + branches: [feature/wasm-pack] tags: - 'v*' - 'cli-v*' + - 'wasm-v*' pull_request: branches: [main] @@ -61,6 +62,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 +109,7 @@ jobs: publish-cli: needs: build-cli runs-on: ubuntu-latest - if: startsWith(github.ref_name, 'cli-v') + if: startsWith(github.ref, 'refs/tags/cli-v') steps: - uses: actions/checkout@v2 - name: Set up Rust @@ -95,3 +122,42 @@ jobs: run: | cd cli/src cargo publish --token ${{ secrets.CRATESTOKEN }} + + + 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 + - 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: 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: Install npm dependencies + run: | + cd wasm/pkg + npm install + npm ci + - name: Publish to npm + run: | + cd wasm/pkg + npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index dfcaa5d..b940a2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,15 +28,20 @@ bindgen = "0.69.4" [features] std = [] plaintext-before-extension = [] +serde = ["dep:serde", "std", "arrayvec/serde", "bitflags/serde"] [profile.release] 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" -arrayvec = "0.7.4" +arrayvec = { version = "0.7.4", optional = false, default-features = true } [workspace] -members = ["cli"] +members = ["cli", "wasm"] 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/). 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/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 0000000..23a6d64
Binary files /dev/null and b/docs/m_bus_parser_wasm_pack_bg.wasm differ
diff --git a/docs/meter.png b/docs/meter.png
new file mode 100644
index 0000000..daa1f54
Binary files /dev/null and b/docs/meter.png differ
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
diff --git a/src/frames/mod.rs b/src/frames/mod.rs
index ffcf22a..a0942c8 100644
--- a/src/frames/mod.rs
+++ b/src/frames/mod.rs
@@ -1,6 +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))]
 #[derive(Debug, PartialEq)]
 pub enum Frame<'a> {
     SingleCharacter {
@@ -22,6 +22,7 @@ pub enum Frame<'a> {
     },
 }
 
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub enum Function {
     SndNk,
@@ -63,7 +64,7 @@ impl TryFrom for Function {
         }
     }
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub enum Address {
     Uninitalized,
diff --git a/src/lib.rs b/src/lib.rs
index 6dae634..9f38693 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)]
@@ -58,9 +53,15 @@ pub mod frames;
 pub mod user_data;
 
 #[derive(Debug)]
+#[cfg_attr(
+    feature = "serde",
+    derive(serde::Serialize, serde::Deserialize),
+    serde(bound(deserialize = "'de: 'a"))
+)]
 pub struct MbusData<'a> {
     pub frame: frames::Frame<'a>,
     pub user_data: Option>,
+    pub data_records: Option,
 }
 
 #[derive(Debug)]
@@ -77,9 +78,7 @@ impl From for MbusError {
 
 impl From for MbusError {
     fn from(error: ApplicationLayerError) -> MbusError {
-        match error {
-            _ => MbusError::ApplicationLayerError(error),
-        }
+        MbusError::ApplicationLayerError(error)
     }
 }
 
@@ -88,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,
+        })
     }
 }
diff --git a/src/user_data/data_information.rs b/src/user_data/data_information.rs
index 2578a56..a2a2761 100644
--- a/src/user_data/data_information.rs
+++ b/src/user_data/data_information.rs
@@ -3,6 +3,7 @@ use super::variable_user_data::DataRecordError;
 use arrayvec::ArrayVec;
 
 const MAX_DIFE_RECORDS: usize = 10;
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct DataInformationBlock {
     pub data_information_field: DataInformationField,
@@ -19,7 +20,7 @@ impl DataInformationBlock {
         size
     }
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct DataInformationField {
     pub data: u8,
@@ -42,7 +43,7 @@ impl From for DataInformationFieldExtension {
         DataInformationFieldExtension { data }
     }
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct DataInformationFieldExtension {
     pub data: u8,
@@ -94,7 +95,7 @@ impl DataInformationFieldExtension {
         self.data & 0x80 != 0
     }
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, Clone, PartialEq)]
 pub struct DataInformation {
     pub storage_number: u64,
@@ -116,7 +117,7 @@ impl std::fmt::Display for DataInformation {
 }
 
 const MAXIMUM_DATA_INFORMATION_SIZE: usize = 11;
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, Clone, PartialEq)]
 pub struct DataInformationExtensionField {}
 
@@ -205,11 +206,13 @@ impl TryFrom<&DataInformationBlock> for DataInformation {
     }
 }
 
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub enum DataType {
     Text(ArrayVec),
     Number(f64),
 }
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(PartialEq, Debug)]
 pub struct Data {
     value: Option,
@@ -509,7 +512,7 @@ impl DataInformation {
         self.size
     }
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub enum FunctionField {
     InstantaneousValue,
@@ -529,7 +532,7 @@ impl std::fmt::Display for FunctionField {
         }
     }
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub enum SpecialFunctions {
     ManufacturerSpecific,
@@ -655,7 +658,7 @@ impl DataFieldCoding {
         }
     }
 }
-
+#[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 ac3991f..53a3255 100644
--- a/src/user_data/data_record.rs
+++ b/src/user_data/data_record.rs
@@ -3,19 +3,19 @@ use super::{
     value_information::{ValueInformation, ValueInformationBlock},
     variable_user_data::DataRecordError,
 };
-
+#[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", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct ProcessedDataRecordHeader {
     pub data_information: DataInformation,
     pub value_information: ValueInformation,
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct DataRecord {
     pub data_record_header: DataRecordHeader,
@@ -27,7 +27,7 @@ impl DataRecord {
         self.data_record_header.get_size() + self.data.get_size()
     }
 }
-
+#[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 35ecc2b..3e4b3c7 100644
--- a/src/user_data/mod.rs
+++ b/src/user_data/mod.rs
@@ -15,6 +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", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct DataRecords {
     pub inner: ArrayVec,
@@ -65,6 +66,7 @@ impl Default for DataRecords {
 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;
@@ -239,6 +241,7 @@ impl fmt::Display for ApplicationLayerError {
 #[cfg(feature = "std")]
 impl std::error::Error for ApplicationLayerError {}
 
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub enum ApplicationResetSubcode {
     All(u8),
@@ -297,12 +300,12 @@ fn bcd_hex_digits_to_u32(digits: [u8; 4]) -> Result
 
     Ok(number)
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct Counter {
     count: u32,
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct IdentificationNumber {
     pub number: u32,
@@ -338,7 +341,7 @@ pub struct FixedDataHeder {
     status: StatusField,
     signature: u16,
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[allow(clippy::large_enum_variant)]
 #[derive(Debug, PartialEq)]
 pub enum UserDataBlock<'a> {
@@ -358,7 +361,7 @@ pub enum UserDataBlock<'a> {
         variable_data_block: &'a [u8],
     },
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub enum Medium {
     Other,
@@ -450,7 +453,7 @@ impl fmt::Display for Medium {
         write!(f, "{}", medium)
     }
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct FixedDataHeader {
     pub identification_number: IdentificationNumber,
@@ -461,7 +464,7 @@ pub struct FixedDataHeader {
     pub status: StatusField,
     pub signature: u16,
 }
-
+#[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 a497269..8e51a7f 100644
--- a/src/user_data/value_information.rs
+++ b/src/user_data/value_information.rs
@@ -70,7 +70,7 @@ fn extract_plaintext_vife(data: &[u8]) -> ArrayVec {
     }
     ascii
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct ValueInformationBlock {
     pub value_information: ValueInformationField,
@@ -78,7 +78,7 @@ pub struct ValueInformationBlock {
         Option>,
     pub plaintext_vife: Option>,
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct ValueInformationField {
     pub data: u8,
@@ -89,7 +89,7 @@ impl ValueInformationField {
         self.data == 0x7C || self.data == 0xFC
     }
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub struct ValueInformationFieldExtension {
     pub data: u8,
@@ -132,7 +132,7 @@ pub enum ValueInformationCoding {
     AlternateVIFExtension,
     ManufacturerSpecific,
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub enum ValueInformationFieldExtensionCoding {
     MainVIFCodeExtension,
@@ -1635,6 +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))]
 #[derive(Debug, PartialEq)]
 pub struct ValueInformation {
     pub decimal_offset_exponent: isize,
@@ -1675,7 +1676,7 @@ impl fmt::Display for ValueInformation {
         Ok(())
     }
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq)]
 pub enum ValueLabel {
     Instantaneous,
@@ -1824,6 +1825,7 @@ pub enum ValueLabel {
     DisplayOutputScalingFactor,
     ManufacturerSpecific,
 }
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, PartialEq, Copy, Clone)]
 pub struct Unit {
     pub name: UnitName,
@@ -1859,7 +1861,7 @@ impl fmt::Display for Unit {
         }
     }
 }
-
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub enum UnitName {
     Watt,
diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml
new file mode 100644
index 0000000..1d27755
--- /dev/null
+++ b/wasm/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "m-bus-parser-wasm-pack"
+version = "0.0.1"
+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"
+m-bus-parser = { path = "..", version = "0.0.9", features = ["std", "serde"] }
+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
+# 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"
diff --git a/wasm/README.md b/wasm/README.md
new file mode 100644
index 0000000..528b8a6
--- /dev/null
+++ b/wasm/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/src/lib.rs b/wasm/src/lib.rs
new file mode 100644
index 0000000..f776aca
--- /dev/null
+++ b/wasm/src/lib.rs
@@ -0,0 +1,33 @@
+use m_bus_parser::{self, MbusData};
+
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+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()) {
+        serde_json::to_string_pretty(&mbus_data).unwrap()
+    } else {
+        "Failed to parse".to_string()
+    }
+}
diff --git a/wasm/tests/web.rs b/wasm/tests/web.rs
new file mode 100644
index 0000000..de5c1da
--- /dev/null
+++ b/wasm/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);
+}