diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..d30f0d7 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,185 @@ +name: Python Bindings CI + +on: + push: + tags: + - 'python-v*' + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.platform.runner }} + defaults: + run: + working-directory: ./python + strategy: + matrix: + platform: + - runner: ubuntu-latest + target: x86_64 + - runner: ubuntu-latest + target: x86 + - runner: ubuntu-latest + target: aarch64 + - runner: ubuntu-latest + target: armv7 + - runner: ubuntu-latest + target: s390x + - runner: ubuntu-latest + target: ppc64le + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: auto + working-directory: ./python + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: ./python/dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + defaults: + run: + working-directory: ./python + strategy: + matrix: + platform: + - runner: ubuntu-latest + target: x86_64 + - runner: ubuntu-latest + target: x86 + - runner: ubuntu-latest + target: aarch64 + - runner: ubuntu-latest + target: armv7 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: musllinux_1_2 + working-directory: ./python + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-musllinux-${{ matrix.platform.target }} + path: ./python/dist + + windows: + runs-on: ${{ matrix.platform.runner }} + defaults: + run: + working-directory: ./python + strategy: + matrix: + platform: + - runner: windows-latest + target: x64 + - runner: windows-latest + target: x86 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + architecture: ${{ matrix.platform.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + working-directory: ./python + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: ./python/dist + + macos: + runs-on: ${{ matrix.platform.runner }} + defaults: + run: + working-directory: ./python + strategy: + matrix: + platform: + - runner: macos-12 + target: x86_64 + - runner: macos-14 + target: aarch64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + working-directory: ./python + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.platform.target }} + path: ./python/dist + + sdist: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./python + steps: + - uses: actions/checkout@v4 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + working-directory: ./python + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: ./python/dist + + release: + name: Release + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./python + needs: [linux, musllinux, windows, macos, sdist] + steps: + - uses: actions/download-artifact@v4 + with: + path: ./python + - name: Publish to PyPI + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --non-interactive --skip-existing wheels-*/* + working-directory: ./python \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4fde777..373d88f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,6 +9,7 @@ on: - 'wasm-v*' pull_request: branches: [main] + workflow_dispatch: jobs: build-library: @@ -122,8 +123,6 @@ jobs: run: | cd cli/src cargo publish --token ${{ secrets.CRATESTOKEN }} - - publish-wasm: needs: build-wasm runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 51124c6..e5a811f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ serde = { version = "1.0", features = ["derive"], optional = true } bitflags = "2.4.2" arrayvec = { version = "0.7.4", default-features = false } [workspace] -members = ["cli", "wasm"] +members = ["cli", "wasm","python"] exclude = ["examples/cortex-m"] [[bench]] diff --git a/README.md b/README.md index 89f8c72..2f26981 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Discord](https://img.shields.io/badge/Discord-Join%20Now-blue?style=flat&logo=Discord)](https://discord.gg/FfmecQ4wua) [![Crates.io](https://img.shields.io/crates/v/m-bus-parser.svg)](https://crates.io/crates/m-bus-parser) [![Downloads](https://img.shields.io/crates/d/m-bus-parser.svg)](https://crates.io/crates/m-bus-parser) [![License](https://img.shields.io/crates/l/m-bus-parser.svg)](https://crates.io/crates/m-bus-parser) [![Documentation](https://docs.rs/m-bus-parser/badge.svg)](https://docs.rs/m-bus-parser) [![Build Status](https://github.com/maebli/m-bus-parser/actions/workflows/rust.yml/badge.svg)](https://github.com/maebli/m-bus-parser/actions/workflows/rust.yml) + ### Introduction *For contributing see [CONTRIBUTING.md](./CONTRIBUTING.md)* @@ -35,6 +36,10 @@ The source is in the wasm folder in this repos 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). +### Python bindings +[![PyPI version](https://badge.fury.io/py/pymbusparser.png)](https://badge.fury.io/py/pymbusparser) + +The are some python bindings, the source is in the sub folder "python" and is published on pypi [https://pypi.org/project/pymbusparser/](https://pypi.org/project/pymbusparser/). ### Visualization of Library Function diff --git a/examples/example_parsing_user_data.rs b/examples/example_parsing_user_data.rs index 6b25a59..8c1928c 100644 --- a/examples/example_parsing_user_data.rs +++ b/examples/example_parsing_user_data.rs @@ -2,7 +2,6 @@ use m_bus_parser::user_data::DataRecords; fn main() { /* Data block 1: unit 0, storage No 0, no tariff, instantaneous volume, 12565 l (24 bit integer) */ - /* Data block 2: unit 0, storage No 0, no tariff, instantaneous volume, 12565 l (24 bit integer) */ let data = vec![0x03, 0x13, 0x15, 0x31, 0x00, 0x03, 0x13, 0x15, 0x31, 0x00]; let result = DataRecords::try_from(data.as_slice()); assert!(result.is_ok()); diff --git a/python/.gitignore b/python/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/python/.gitignore @@ -0,0 +1 @@ +/target diff --git a/python/Cargo.toml b/python/Cargo.toml new file mode 100644 index 0000000..8c1d30f --- /dev/null +++ b/python/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "pymbusparser" +version = "0.0.1" +edition = "2021" +homepage = "https://maebli.github.io/" +repository = "https://github.com/maebli/m-bus-parser" + +[dependencies] +m-bus-parser = { path = "..", version = "0.0.13", features = ["std", "serde"] } +serde_json = "1.0" +pyo3 = { version = "0.22.2", features = ["extension-module","generate-import-lib"] } +hex = "0.4.2" + +[lib] +name = "pymbusparser" \ No newline at end of file diff --git a/python/README_python.md b/python/README_python.md new file mode 100644 index 0000000..2283225 --- /dev/null +++ b/python/README_python.md @@ -0,0 +1,35 @@ +# Python bindings [WIP] + +Rust lib aims to be accessible from a Python module. This is done by using the `PyO3` crate. + +## Development + +- `pipx install maturin` to install `maturin` globally. +- `maturin develop` to build the Rust lib and create a Python module in the current environment. +- Currently this creates a release in the target directory that is one hierachy up. This is not ideal and will be fixed in the future. +- after calling the maturin develop command cd one up and `pip install target/..` and then run `python` and `from pymbusparser import pymbus` to test the module. +- to test inside `REPL` run `python` and then `import p + +## Publishing + +- `maturin publish` +- username = __token__ +- passwrod = api key from pypi + +## Usage + +`pip install pymbusparser` + +```python +from pymbusparser import m_bus_parse,parse_application_layer + +print(m_bus_parse("68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16","table")) + +print(m_bus_parse("68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16","json")) + +print(m_bus_parse("68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E )16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16","yml")) + +# note this is not as pretty as the function before, still TODO, currently it just outputs structs of RUST in string +print(parse_application_layer("2f2f0413fce0f5052f2f2f2f2f2f2f2f")) + +``` \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..068ca86 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "pymbusparser" +version = "0.0.1" +description = "Python bindings for m-bus-parser written in Rust" +homepage = "https://maebli.github.io/" +authors = [{name = "Michael Aebli", email = ""}] +license = { text = "MIT" } +readme = "README_python.md" + + +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" \ No newline at end of file diff --git a/python/src/lib.rs b/python/src/lib.rs new file mode 100644 index 0000000..0fe17a8 --- /dev/null +++ b/python/src/lib.rs @@ -0,0 +1,46 @@ +use hex; +use m_bus_parser::serialize_mbus_data; +use m_bus_parser::user_data::DataRecords; +use pyo3::prelude::*; +use serde_json; + +#[pyfunction] +fn parse_application_layer(data_record: &str) -> PyResult { + // Decode the hex string into bytes + match hex::decode(data_record) { + Ok(bytes) => { + // Try to parse the bytes into DataRecords + match DataRecords::try_from(bytes.as_slice()) { + Ok(records) => { + // Serialize the records to JSON using Serde + match serde_json::to_string(&records) { + Ok(json) => Ok(json), + Err(e) => Err(PyErr::new::(format!( + "Failed to serialize records to JSON: {}", + e + ))), + } + } + Err(_) => Err(PyErr::new::( + "Failed to parse data record", + )), + } + } + Err(e) => Err(PyErr::new::(format!( + "Failed to decode hex: {}", + e + ))), + } +} + +#[pyfunction] +pub fn m_bus_parse(data: &str, format: &str) -> String { + serialize_mbus_data(data, format) +} + +#[pymodule] +fn pymbusparser(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(parse_application_layer, m)?)?; + m.add_function(wrap_pyfunction!(m_bus_parse, m)?)?; + Ok(()) +}