From aed024ba594d9842dacbc25355407c03ba7bffa7 Mon Sep 17 00:00:00 2001 From: Dima Dorezyuk Date: Tue, 19 Mar 2024 08:40:42 +0100 Subject: [PATCH] RsIskraMeter: Initial commit (#580) * RsIskraMeter: initial commit Signed-off-by: Dima Dorezyuk * fixup the interfaces Signed-off-by: Dima Dorezyuk * Add doc Signed-off-by: Dima Dorezyuk * fixup Signed-off-by: Dima Dorezyuk * fix clang Signed-off-by: Dima Dorezyuk * fix pipeline name Signed-off-by: Dima Dorezyuk * update everest-framework reference Signed-off-by: Dima Dorezyuk * update lock Signed-off-by: Dima Dorezyuk * reviewer remarks Signed-off-by: Dima Dorezyuk * remove public_key topic Signed-off-by: Dima Dorezyuk * reviewer remarks Signed-off-by: Dima Dorezyuk * Update main.rs Signed-off-by: Dima Dorezyuk --------- Signed-off-by: Dima Dorezyuk Signed-off-by: Dima Dorezyuk Co-authored-by: Dima Dorezyuk --- ...l_build.yaml => bazel_build_and_test.yaml} | 4 +- WORKSPACE.bazel | 5 +- modules/BUILD.bazel | 0 modules/CMakeLists.txt | 1 + modules/Cargo.lock | 822 +++++++++++ modules/Cargo.toml | 11 + .../main/lem_dcbm_400600_controller.cpp | 3 +- modules/RsIskraMeter/BUILD.bazel | 63 + modules/RsIskraMeter/Cargo.toml | 20 + modules/RsIskraMeter/build.rs | 13 + modules/RsIskraMeter/manifest.yaml | 46 + modules/RsIskraMeter/src/logger.rs | 48 + modules/RsIskraMeter/src/main.rs | 1207 +++++++++++++++++ modules/RsIskraMeter/src/utils.rs | 166 +++ modules/rust_examples/Cargo.lock | 310 ----- modules/rust_examples/Cargo.toml | 10 - third-party/bazel/repos.bzl | 4 +- types/powermeter.yaml | 6 + 18 files changed, 2413 insertions(+), 326 deletions(-) rename .github/workflows/{bazel_build.yaml => bazel_build_and_test.yaml} (87%) create mode 100644 modules/BUILD.bazel create mode 100644 modules/Cargo.lock create mode 100644 modules/Cargo.toml create mode 100644 modules/RsIskraMeter/BUILD.bazel create mode 100644 modules/RsIskraMeter/Cargo.toml create mode 100644 modules/RsIskraMeter/build.rs create mode 100644 modules/RsIskraMeter/manifest.yaml create mode 100644 modules/RsIskraMeter/src/logger.rs create mode 100644 modules/RsIskraMeter/src/main.rs create mode 100644 modules/RsIskraMeter/src/utils.rs delete mode 100644 modules/rust_examples/Cargo.lock delete mode 100644 modules/rust_examples/Cargo.toml diff --git a/.github/workflows/bazel_build.yaml b/.github/workflows/bazel_build_and_test.yaml similarity index 87% rename from .github/workflows/bazel_build.yaml rename to .github/workflows/bazel_build_and_test.yaml index d7a83f57b..b64ad6b13 100644 --- a/.github/workflows/bazel_build.yaml +++ b/.github/workflows/bazel_build_and_test.yaml @@ -2,7 +2,7 @@ name: Bazel Build run-name: ${{ github.actor }} is building with bazel on: [pull_request] jobs: - bazel-build: + bazel-build-and-test: runs-on: ubuntu-22.04 steps: - run: echo branch name is ${{ github.ref }} @@ -16,3 +16,5 @@ jobs: - name: Build all run: > bazelisk build //... + - name: Test all + run: bazelisk test //... diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 22498f7a3..437b86620 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -24,10 +24,11 @@ load("@rules_rust//crate_universe:defs.bzl", "crates_repository", "crate") crates_repository( name = "crate_index", - cargo_lockfile = "//modules/rust_examples:Cargo.lock", + cargo_lockfile = "//modules:Cargo.lock", isolated = False, manifests = [ - "//modules/rust_examples:Cargo.toml", + "//modules:Cargo.toml", + "//modules/RsIskraMeter:Cargo.toml", "//modules/rust_examples/RsExample:Cargo.toml", "//modules/rust_examples/RsExampleUser:Cargo.toml", ], diff --git a/modules/BUILD.bazel b/modules/BUILD.bazel new file mode 100644 index 000000000..e69de29bb diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 6b53d744a..4a142cbb9 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -35,5 +35,6 @@ add_subdirectory(examples) add_subdirectory(simulation) if(${EVEREST_ENABLE_RS_SUPPORT}) + ev_add_module(RsIskraMeter) add_subdirectory(rust_examples) endif() diff --git a/modules/Cargo.lock b/modules/Cargo.lock new file mode 100644 index 000000000..6cfc3ecf4 --- /dev/null +++ b/modules/Cargo.lock @@ -0,0 +1,822 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "RsExample" +version = "0.1.0" +dependencies = [ + "everestrs", + "everestrs-build", + "serde", + "serde_json", +] + +[[package]] +name = "RsExampleUser" +version = "0.1.0" +dependencies = [ + "everestrs", + "everestrs-build", + "serde", + "serde_json", +] + +[[package]] +name = "RsIskraMeter" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "env_logger", + "everestrs", + "everestrs-build", + "log", + "mockall", + "mockall_double", + "rand", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "argh" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a" +dependencies = [ + "argh_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531" +dependencies = [ + "serde", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bumpalo" +version = "3.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cxx" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "everestrs" +version = "0.1.0" +source = "git+https://github.com/everest/everest-framework.git?rev=01468678f5b627a96791f749f3139c3d169bf081#01468678f5b627a96791f749f3139c3d169bf081" +dependencies = [ + "argh", + "cxx", + "everestrs-build", + "log", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "everestrs-build" +version = "0.1.0" +source = "git+https://github.com/everest/everest-framework.git?rev=01468678f5b627a96791f749f3139c3d169bf081#01468678f5b627a96791f749f3139c3d169bf081" +dependencies = [ + "anyhow", + "argh", + "convert_case", + "minijinja", + "serde", + "serde_json", + "serde_yaml", +] + +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "link-cplusplus" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" +dependencies = [ + "cc", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "minijinja" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "208758577ef2c86cf5dd3e85730d161413ec3284e2d73b2ef65d9a24d9971bcb" +dependencies = [ + "serde", +] + +[[package]] +name = "mockall" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "mockall_double" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ca96e5ac35256ae3e13536edd39b172b88f41615e1d7b653c8ad24524113e8" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "predicates" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/modules/Cargo.toml b/modules/Cargo.toml new file mode 100644 index 000000000..740fc7156 --- /dev/null +++ b/modules/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +resolver = "2" +members = [ + "RsIskraMeter", + "rust_examples/RsExample", + "rust_examples/RsExampleUser", +] + +[workspace.dependencies] +everestrs = { git = "https://github.com/everest/everest-framework.git", rev = "01468678f5b627a96791f749f3139c3d169bf081" } +everestrs-build = { git = "https://github.com/everest/everest-framework.git", rev = "01468678f5b627a96791f749f3139c3d169bf081" } diff --git a/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp b/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp index f4738d5ff..4af666c3e 100644 --- a/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp +++ b/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp @@ -56,7 +56,8 @@ LemDCBM400600Controller::start_transaction(const types::powermeter::TransactionR auto [transaction_min_stop_time, transaction_max_stop_time] = get_transaction_stop_time_bounds(); - return {types::powermeter::TransactionRequestStatus::OK, {}, transaction_min_stop_time, transaction_max_stop_time}; + return { + types::powermeter::TransactionRequestStatus::OK, {}, {}, transaction_min_stop_time, transaction_max_stop_time}; } void LemDCBM400600Controller::request_device_to_start_transaction(const types::powermeter::TransactionReq& value) { diff --git a/modules/RsIskraMeter/BUILD.bazel b/modules/RsIskraMeter/BUILD.bazel new file mode 100644 index 000000000..647583001 --- /dev/null +++ b/modules/RsIskraMeter/BUILD.bazel @@ -0,0 +1,63 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test") +load("@crate_index//:defs.bzl", "all_crate_deps") +load("@rules_rust//cargo:defs.bzl", "cargo_build_script") + +cargo_build_script( + name = "build_script", + srcs = ["build.rs"], + edition="2021", + build_script_env = { + "EVEREST_CORE_ROOT": "../..", + }, + data = [ + "manifest.yaml", + "@everest-core//:interfaces", + "@everest-core//:types", + ], + deps = all_crate_deps(build = True), +) + +rust_binary( + name = "RsIskraMeterBinary", + srcs = glob(["src/*.rs"]), + edition="2021", + proc_macro_deps = all_crate_deps(proc_macro = True), + visibility = ["//visibility:public"], + deps = all_crate_deps(normal = True) + [ + ":build_script", + "@everest-framework//everestrs/everestrs:everestrs_sys", + "@everest-framework//everestrs/everestrs:everestrs_bridge", + ], +) + +rust_test( + name = "RsIskraMeterTest", + edition="2021", + srcs = [], + crate_features = ["mockall", "mockall_double"], + crate = ":RsIskraMeterBinary", +) + +binary = ":RsIskraMeterBinary" +manifest = ":manifest.yaml" +name = "RsIskraMeter" + +genrule( + name = "copy_to_subdir", + srcs = [binary, manifest], + outs = [ + "{}/manifest.yaml".format(name), + "{}/{}".format(name, name), + ], + cmd = "mkdir -p $(RULEDIR)/{} && ".format(name) + + "cp $(location {}) $(RULEDIR)/{}/{} && ".format(binary, name, name) + + "cp $(location {}) $(RULEDIR)/{}/".format(manifest, name), +) + +filegroup( + name = name, + srcs = [ + ":copy_to_subdir", + ], + visibility = ["//visibility:public"], +) diff --git a/modules/RsIskraMeter/Cargo.toml b/modules/RsIskraMeter/Cargo.toml new file mode 100644 index 000000000..ce040b0b1 --- /dev/null +++ b/modules/RsIskraMeter/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "RsIskraMeter" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +everestrs-build = { workspace=true } + +[dependencies] +anyhow = "1.0.75" +chrono = "0.4.31" +everestrs = { workspace=true } +log = "0.4.20" +env_logger = "0.10.0" +rand = "0.8.5" +mockall = { version = "0.12.1", optional = true } +mockall_double = { version = "0.3.1", optional = true} + +[features] +default = ["mockall", "mockall_double"] diff --git a/modules/RsIskraMeter/build.rs b/modules/RsIskraMeter/build.rs new file mode 100644 index 000000000..8427ecfd3 --- /dev/null +++ b/modules/RsIskraMeter/build.rs @@ -0,0 +1,13 @@ +use everestrs_build::Builder; + +pub fn main() { + Builder::new( + "manifest.yaml", + vec![std::env::var("EVEREST_CORE_ROOT").unwrap()], + ) + .generate() + .unwrap(); + + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=manifest.yaml"); +} diff --git a/modules/RsIskraMeter/manifest.yaml b/modules/RsIskraMeter/manifest.yaml new file mode 100644 index 000000000..81dcd8ff9 --- /dev/null +++ b/modules/RsIskraMeter/manifest.yaml @@ -0,0 +1,46 @@ +description: Iskra meter +config: + powermeter_device_id: + description: The address of the device on the modbus + default: 33 + type: integer + ocmf_format_version: + description: Version of the data format in the representation. + default: "1.0" + type: string + ocmf_gateway_identification: + description: >- + Identifier of the manufacturer for the system which has generated the + present data. + default: "" + type: string + ocmf_gateway_serial: + description: Serial number of the above mentioned system. + default: "" + type: string + ocmf_gateway_version: + description: >- + Version designation of the manufacturer for the software. + default: "" + type: string + ocmf_charge_point_identification_type: + description: >- + Type of the specification for the identification of the charge point. + default: "EVSEID" + type: string + ocmf_charge_point_identification: + description: Identification information for the charge point. + default: "" + type: string +provides: + meter: + description: Implementation of the driver functionality + interface: powermeter +requires: + modbus: + interface: serial_communication_hub +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - embedded@qwello.eu +enable_external_mqtt: false diff --git a/modules/RsIskraMeter/src/logger.rs b/modules/RsIskraMeter/src/logger.rs new file mode 100644 index 000000000..274c52a5b --- /dev/null +++ b/modules/RsIskraMeter/src/logger.rs @@ -0,0 +1,48 @@ +//! Logger with journald compliant output. +use env_logger::{Builder, Env}; +use everestrs::serde as __serde; +use std::io::Write; + +#[derive(__serde::Serialize)] +#[serde(crate = "__serde", rename_all = "UPPERCASE")] +struct Entry<'a> { + code_file: &'a str, + code_line: u32, + message: &'a str, +} + +/// Initializes the logging. +/// +/// # Arguments +/// * `env_var` - The environment variable defining the minimal logging +/// severity. Must be one of `trace`, `debug`, `info`, `warn` or `error`. If +/// the environment variable is not set, the default severity will be `info`. +pub fn init_logger(env_var: &str) { + let env = Env::default().filter_or(env_var, "info"); + + Builder::from_env(env) + .format(|buf, record| { + let message = &record.args().to_string(); + + let entry = Entry { + code_file: record.file().unwrap_or("unknown"), + code_line: record.line().unwrap_or(0), + message, + }; + + writeln!( + buf, + "<{}>{}", + match record.level() { + log::Level::Error => 3, + log::Level::Warn => 4, + log::Level::Info => 6, + log::Level::Debug => 7, + log::Level::Trace => 7, + }, + ::everestrs::serde_json::to_string(&entry) + .unwrap_or("Failed to format the log message".to_string()), + ) + }) + .init(); +} diff --git a/modules/RsIskraMeter/src/main.rs b/modules/RsIskraMeter/src/main.rs new file mode 100644 index 000000000..71c80abe7 --- /dev/null +++ b/modules/RsIskraMeter/src/main.rs @@ -0,0 +1,1207 @@ +//! Module for Iskra's WM3M4 & WM3M4C three-phase electrical energy meters. +//! +//! The implementation follows the user manual +//! +//! +//! ## Example usage: +//! +//! To try out the Iskra meter you can use the following dummy config below. +//! +//! ```yaml +//!active_modules: +//! power_meter_1: +//! module: RsIskraMeter +//! config_module: +//! powermeter_device_id: 33 +//! connections: +//! modbus: +//! - module_id: serial_comm_hub_1 +//! implementation_id: main +//! serial_comm_hub_1: +//! module: SerialCommHub +//! config_implementation: +//! main: +//! serial_port: /dev/ttyUSB0 +//! baudrate: 115200 +//! initial_timeout_ms: 10000 +//! within_message_timeout_ms: 100 +//! max_packet_size: 100 +//! ``` +//! +//! You can start and stop transactions by publishing following messages to +//! mqtt +//! +//! ```sh +//! mosquitto_pub -t everest/power_meter_1/meter/cmd -m '{"data":{"args":{"value":{"cable_id":1,"client_id":"","evse_id":"DEABCD312ABC11","tariff_id":1,"transaction_id":"foobarbaz","user_data":""}},"id":"foo","origin":"bar"},"name":"start_transaction","type":"call"}' +//! mosquitto_pub -t everest/power_meter_1/meter/cmd -m '{"data":{"args":{"transaction_id":"foobarbaz"}, "id":"foo","origin":"bar"},"name":"stop_transaction","type":"call"}' +//! ``` +#![allow(non_snake_case, non_camel_case_types)] + +include!(concat!(env!("OUT_DIR"), "/generated.rs")); +mod logger; +mod utils; + +use anyhow::Result; +use chrono::{Local, Offset, Utc}; +use everestrs::serde as everest_serde; +use everestrs::serde_json as everest_serde_json; +use generated::types::powermeter::{ + Powermeter, TransactionRequestStatus, TransactionStartResponse, TransactionStopResponse, +}; +use generated::types::serial_comm_hub_requests::{StatusCodeEnum, VectorUint16}; +use generated::types::units::{Current, Energy, Frequency, Power, ReactivePower, Voltage}; +use generated::types::units_signed::SignedMeterValue; +use generated::{get_config, Module, ModuleConfig, SerialCommunicationHubClientPublisher}; +use std::fmt::Debug; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use utils::{ + counter, create_ocmf, create_random_meter_session_id, from_t5_format, from_t6_format, + string_to_vec, to_8_string, to_hex_string, +}; + +/// Public key prefix for transparency software, defined under 6.5.14. +const PUBLIC_KEY_PREFIX: &str = "3059301306072A8648CE3D020106082A8648CE3D03010703420004"; + +/// The charging state from register 7000, defined at table 6. +#[derive(PartialEq, Debug)] +enum IskraMaterState { + Idle, + Active, + Active_after_power_failure, + Active_after_reset, + Unknown, +} + +impl IskraMaterState { + fn from_register(val: u16) -> Self { + match val { + 0 => IskraMaterState::Idle, + 1 => IskraMaterState::Active, + 2 => IskraMaterState::Active_after_power_failure, + 3 => IskraMaterState::Active_after_reset, + _ => IskraMaterState::Unknown, + } + } +} + +#[derive(PartialEq, Debug)] +/// The signature status values defined at table 11. +enum SignatureStatus { + NotInitialized, + Idle, + SignatureInProgress, + SignatureOK, + InvalidDateTime, + CheckSumError, + InvalidCommand, + InvalidState, + InvalidMeasurement, + TestModeError, + VerifyStateError, + SignatureStateError, + KeyPairGenerationError, + SHAFailed, + InitFailed, + DataNotLocked, + ConfigNotLocked, + VerifyError, + PublicKeyError, + InvalidMessageFormat, + InvalidMessageSize, + SignatureError, + UndefinedError, +} + +impl TryFrom for SignatureStatus { + type Error = anyhow::Error; + fn try_from(value: u16) -> std::result::Result { + match value { + 0 => Ok(SignatureStatus::NotInitialized), + 1 => Ok(SignatureStatus::Idle), + 2 => Ok(SignatureStatus::SignatureInProgress), + 15 => Ok(SignatureStatus::SignatureOK), + 128 => Ok(SignatureStatus::InvalidDateTime), + 129 => Ok(SignatureStatus::CheckSumError), + 130 => Ok(SignatureStatus::InvalidCommand), + 131 => Ok(SignatureStatus::InvalidState), + 132 => Ok(SignatureStatus::InvalidMeasurement), + 133 => Ok(SignatureStatus::TestModeError), + 243 => Ok(SignatureStatus::VerifyStateError), + 244 => Ok(SignatureStatus::SignatureStateError), + 245 => Ok(SignatureStatus::KeyPairGenerationError), + 246 => Ok(SignatureStatus::SHAFailed), + 247 => Ok(SignatureStatus::InitFailed), + 248 => Ok(SignatureStatus::DataNotLocked), + 249 => Ok(SignatureStatus::ConfigNotLocked), + 250 => Ok(SignatureStatus::VerifyError), + 251 => Ok(SignatureStatus::PublicKeyError), + 252 => Ok(SignatureStatus::InvalidMessageFormat), + 253 => Ok(SignatureStatus::InvalidMessageSize), + 254 => Ok(SignatureStatus::SignatureError), + 255 => Ok(SignatureStatus::UndefinedError), + unknown => Err(anyhow::anyhow!("Failed to convert the value {unknown}")), + } + } +} + +/// Converts the EVerest type to our internal. +impl From for Result<()> { + fn from(value: StatusCodeEnum) -> Self { + match value { + StatusCodeEnum::Success => Ok(()), + StatusCodeEnum::Error => anyhow::bail!("StatusCodeEnum::Error"), + } + } +} + +/// Custom extension of the auto generated type. +impl generated::types::serial_comm_hub_requests::Result { + /// We have to check if the received data matches the expected size. + /// * `size`: The expected size of the vector. + fn into_vec(self, size: usize) -> Result> { + match self.status_code { + StatusCodeEnum::Success => match self.value { + None => anyhow::bail!("Received None as value"), + Some(value) => { + if value.len() != size { + anyhow::bail!("Expected size {}, received size {size}", value.len()) + } + Ok(value.into_iter().map(|v| v as u16).collect()) + } + }, + StatusCodeEnum::Error => anyhow::bail!("StatusCodeEnum::Error"), + } + } +} + +/// Custom conversion to `Result<[u16; N]>` +/// +/// Similar to `generated::types::serial_comm_hub_requests::Result::into_vec` +/// but returns an array. +impl From for Result<[u16; N]> { + fn from(value: generated::types::serial_comm_hub_requests::Result) -> Self { + match value.status_code { + StatusCodeEnum::Success => match value.value { + None => anyhow::bail!("Received None as value"), + Some(inner) => { + if inner.len() != N { + anyhow::bail!("Expected size {}, received size {N}", inner.len()) + } + let mut res = [0; N]; + for (ss, dd) in std::iter::zip(inner, &mut res) { + *dd = ss as u16; + } + Ok(res) + } + }, + StatusCodeEnum::Error => anyhow::bail!("StatusCodeEnum::Error"), + } + } +} + +/// Custom extension of the auto generated `TransactionStartResponse`. +impl TransactionStartResponse { + /// Constructs an error response from the input. + /// * `error`: The error type. + fn from_err(error: &E) -> Self + where + E: Debug, + { + Self { + error: Some(format!("{error:?}")), + signed_meter_value: None, + status: TransactionRequestStatus::UNEXPECTED_ERROR, + transaction_max_stop_time: None, + transaction_min_stop_time: None, + } + } +} + +/// Custom extension of the auto generated `TransactionStopResponse`. +impl TransactionStopResponse { + /// Constructs an error response from the input. + /// * `error`: The error type. + fn from_err(error: &E) -> Self + where + E: Debug, + { + Self { + error: Some(format!("{error:?}")), + signed_meter_value: None, + status: TransactionRequestStatus::UNEXPECTED_ERROR, + } + } +} + +#[derive(everest_serde::Serialize, Clone)] +#[serde(crate = "everest_serde")] +/// The serialization according to the Open charge metering format. See +/// https://github.com/SAFE-eV/OCMF-Open-Charge-Metering-Format/blob/master/OCMF-en.md +/// for details. +/// +/// The struct does not implement everything and also our config does not +/// implement everything. The allowed fields (and which can be set by the user) +/// are defined under the Iskra's manual chapter 6.5.15. +struct OcmfData { + #[serde(rename = "FV")] + /// Version of the data format in the representation. + format_version: String, + + #[serde(rename = "GI")] + /// Identifier of the manufacturer for the system which has generated the + /// present data + gateway_identification: String, + + #[serde(rename = "GS")] + /// Serial number of the above mentioned system. + gateway_serial: String, + + #[serde(rename = "GV")] + /// Version designation of the manufacturer for the software. + gateway_version: String, + + #[serde(rename = "PG")] + /// Pagination of the entire data set, i.e. the data that is combined in one + /// signature. + pagination: String, + + #[serde(rename = "MV")] + /// Manufacturer identification of the meter, name of the manufacturer. + meter_vendor: String, + + #[serde(rename = "MM")] + /// Model identification of the meter. + meter_model: String, + + #[serde(rename = "MS")] + /// Serial number of the meter. + meter_serial: String, + + #[serde(rename = "MF")] + /// Firmware version of the meter. + meter_firmware: String, + + #[serde(rename = "IS")] + /// General status for user assignment. + identification_status: bool, + + #[serde(rename = "IF")] + /// Detailed statements about the user assignment. + identification_flags: Vec, + + #[serde(rename = "IT")] + /// Type of identification data. + identification_type: String, + + #[serde(rename = "ID")] + /// The actual identification data according to the type. + identification_data: String, + + #[serde(rename = "CT")] + /// Type of the specification for the identification of the charge point. + charge_point_identification_type: String, + + #[serde(rename = "CI")] + /// Identification information for the charge point. + charge_point_identification: String, + + #[serde(rename = "RD")] + /// Additional readings. + readings: Vec, +} + +impl Default for OcmfData { + fn default() -> Self { + Self { + format_version: "1.0".to_string(), + gateway_identification: String::default(), + gateway_serial: String::default(), + gateway_version: String::default(), + pagination: String::default(), + meter_vendor: String::default(), + meter_model: String::default(), + meter_serial: String::default(), + meter_firmware: String::default(), + identification_status: true, + // See table 13 for details. + identification_flags: vec!["RFID_PLAIN".to_string()], + // See table 17 for details. + identification_type: "NONE".to_string(), + identification_data: String::default(), + // See table 18 for details. + charge_point_identification_type: "EVSEID".to_string(), + charge_point_identification: String::default(), + readings: Vec::default(), + } + } +} + +/// Conversion from the EVerest config to the OcmfData. +impl From<&ModuleConfig> for OcmfData { + fn from(value: &ModuleConfig) -> Self { + // Below we have to replace the `|` with something (here whitespace), + // since otherwise the transpacency tool will not accept the signed + // data. See https://safe-ev.org/de/transparenzsoftware/e-mobilist/. + let sanitize = |user_string: &str| { + if user_string.contains("|") { + log::warn!("Removing the forbidden symbol`|` from {user_string}"); + } + user_string.replace("|", " ") + }; + + OcmfData { + format_version: sanitize(&value.ocmf_format_version), + gateway_identification: sanitize(&value.ocmf_gateway_identification), + gateway_serial: sanitize(&value.ocmf_gateway_serial), + gateway_version: sanitize(&value.ocmf_gateway_version), + charge_point_identification_type: sanitize(&value.ocmf_charge_point_identification_type), + charge_point_identification: sanitize(&value.ocmf_charge_point_identification), + ..Default::default() + } + } +} + +/// Toy retry module +mod retry { + + /// The state the functions have to return. + pub enum RetryState { + /// If you return this, we keep trying until the timeout. + KeepTrying, + + /// If you return this, we're done. + Done, + } + + type RetryResult = anyhow::Result; + + pub fn retry(func: F, duration: std::time::Duration) -> anyhow::Result<()> + where + F: Fn() -> RetryResult, + { + let deadline = std::time::SystemTime::now() + duration; + let sleep = std::cmp::min(duration, std::time::Duration::from_millis(10)); + while std::time::SystemTime::now() < deadline { + match func()? { + RetryState::Done => return Ok(()), + RetryState::KeepTrying => { + std::thread::sleep(sleep); + } + } + } + anyhow::bail!("Retry failed - timeout after {duration:?}"); + } +} + +// Below we're using the type state pattern to implement a small state machine +// with the states `InitState` and `ReadyState`. + +/// Initial state. We're moving from this state to `ReadyState` once we're fully +/// Initialized. +#[derive(Clone)] +struct InitState { + /// Modbus id of the device. + device_id: i64, + + /// The base ocmf data. + ocmf_data: OcmfData, +} + +impl InitState { + fn new(device_id: i64, ocmf_data: OcmfData) -> Self { + Self { + device_id, + ocmf_data, + } + } +} + +/// State where we're ready for publishing. +#[derive(Clone)] +struct ReadyState { + /// Modbus id of the device. + device_id: i64, + + /// The base ocmf data. + ocmf_data: OcmfData, + + /// The interface to the serial comm module. + serial_comm_pub: SerialCommunicationHubClientPublisher, + + /// Public key + public_key: Arc>>, +} + +impl ReadyState { + /// The `ReadyState` can only be constructed from `InitState`. + fn new(init_state: InitState, serial_comm_pub: SerialCommunicationHubClientPublisher) -> Self { + Self { + device_id: init_state.device_id, + ocmf_data: init_state.ocmf_data, + serial_comm_pub, + public_key: Arc::new(Mutex::new(None)), + } + } + + /// Reads `N` registers, starting at `first_register_address`. + fn read_input_registers_fixed( + &self, + first_register_address: u16, + ) -> Result<[u16; N]> { + self.serial_comm_pub + .modbus_read_input_registers(first_register_address as i64, N as i64, self.device_id)? + .into() + } + + fn read_holding_registers( + &self, + first_register_address: u16, + num_registers_to_read: u16, + ) -> Result> { + self.serial_comm_pub + .modbus_read_holding_registers( + first_register_address as i64, + num_registers_to_read as i64, + self.device_id, + )? + .into_vec(num_registers_to_read as usize) + } + + /// Same as `read_holding_registers` but returns a compile-time known slice. + /// Use indices to access the members and the compiler will check for out + /// of bounds. + fn read_holding_registers_fixed( + &self, + first_register_address: u16, + ) -> Result<[u16; N]> { + self.serial_comm_pub + .modbus_read_holding_registers(first_register_address as i64, N as i64, self.device_id)? + .into() + } + + fn write_multiple_registers(&self, first_register_address: u16, data: &[u16]) -> Result<()> { + self.serial_comm_pub + .modbus_write_multiple_registers( + VectorUint16 { + data: data.iter().copied().map(|v| v as i64).collect(), + }, + first_register_address as i64, + self.device_id, + )? + .into() + } + + fn write_single_register(&self, register_address: u16, data: u16) -> Result<()> { + self.serial_comm_pub + .modbus_write_single_register(data as i64, register_address as i64, self.device_id)? + .into() + } + + fn read_state(&self) -> Result { + let var = self.read_holding_registers_fixed::<1>(7000)?; + let state = IskraMaterState::from_register(var[0]); + log::info!("State register {:?} mapped to state {state:?}", var[0]); + Ok(state) + } + + fn read_command_status(&self) -> Result { + let var = self.read_holding_registers_fixed::<1>(7052)?; + let state = var[0].try_into()?; + log::info!( + "Command status register {:?} mapped to state {state:?}", + var[0] + ); + Ok(state) + } + + fn set_time(&self) -> Result<()> { + let ts = Utc::now().timestamp(); + let offset_min = Local::now().offset().fix().local_minus_utc() / 60; + // Write time + log::info!( + "Writing time {:X}, {:X} {:X} with offset {offset_min}", + ts, + (ts >> 16 & 0xFFFF), + (ts & 0xFFFF) + ); + self.write_single_register(7054, (ts >> 16 & 0xFFFF) as u16)?; + self.write_single_register(7055, (ts & 0xFFFF) as u16)?; + // Write timezone + self.write_single_register(7053, offset_min as u16)?; + // Set time as synchronized + self.write_single_register(7071, 2)?; + Ok(()) + } + + fn read_device_group(&self) -> Result { + let registers = self.read_input_registers_fixed::<1>(1)?; + to_8_string(®isters) + } + + fn read_device_model(&self) -> Result { + let registers = self.read_input_registers_fixed::<8>(1)?; + to_8_string(®isters) + } + + fn read_device_serial(&self) -> Result { + let registers = self.read_input_registers_fixed::<4>(9)?; + to_8_string(®isters) + } + + fn read_t6(&self, first_register_address: u16) -> Result { + let var = self.read_input_registers_fixed::<2>(first_register_address)?; + Ok(from_t6_format(var)) + } + + fn read_t5(&self, first_register_address: u16) -> Result { + let var = self.read_input_registers_fixed::<2>(first_register_address)?; + Ok(from_t5_format(var)) + } + + fn read_counter( + &self, + exp_register_address: u16, + counter_register_address: u16, + ) -> Result { + let exp_register = self.read_input_registers_fixed::<1>(exp_register_address)?; + // Signed Value (16 bits) + let var = self.read_input_registers_fixed::<2>(counter_register_address)?; + Ok(counter(var, exp_register[0])) + } + + fn read_meter_value(&self) -> Result { + let resp = Powermeter { + var: Some(ReactivePower { + l_1: Some(self.read_t6(150)?), + l_2: Some(self.read_t6(152)?), + l_3: Some(self.read_t6(154)?), + total: self.read_t6(148)?, + }), + current_a: Some(Current { + dc: Option::None, + l_1: Some(self.read_t6(126)?), + l_2: Some(self.read_t6(128)?), + l_3: Some(self.read_t6(130)?), + n: Option::None, + }), + energy_wh_export: Some(Energy { + l_1: Option::None, + l_2: Option::None, + l_3: Option::None, + total: self.read_counter(415, 420)?, + }), + energy_wh_import: Energy { + l_1: Option::None, + l_2: Option::None, + l_3: Option::None, + total: self.read_counter(414, 418)?, + }, + // TODO(kch) why freq values are different? + frequency_hz: Some(Frequency { + l_1: self.read_t5(105)?, + l_2: Option::None, + l_3: Option::None, + }), + // TODO(kch) meter_id + meter_id: Option::None, + phase_seq_error: Option::None, + power_w: Some(Power { + l_1: Some(self.read_t6(142)?), + l_2: Some(self.read_t6(144)?), + l_3: Some(self.read_t6(146)?), + total: self.read_t6(140)?, + }), + timestamp: Utc::now().to_rfc3339(), + voltage_v: Some(Voltage { + dc: Option::None, + l_1: Some(self.read_t5(107)?), + l_2: Some(self.read_t5(109)?), + l_3: Some(self.read_t5(111)?), + }), + current_a_signed: None, + energy_wh_export_signed: None, + energy_wh_import_signed: None, + frequency_hz_signed: None, + power_w_signed: None, + signed_meter_value: None, + var_signed: None, + voltage_v_signed: None, + }; + Ok(resp) + } + + fn write_metadata(&self, evse_id: &str) -> Result<()> { + let mut ocmf_data = self.ocmf_data.clone(); + ocmf_data.identification_data = format!("{} {}", evse_id, create_random_meter_session_id()); + + let message = everest_serde_json::to_string(&ocmf_data)?; + let data = string_to_vec(&message); + self.write_multiple_registers(7100, &data)?; + // write bytes + log::info!("Writing length {:?}", message.len()); + self.write_single_register(7056, message.len() as u16)?; + let resp = self.read_holding_registers(7100, data.len() as u16)?; + let mut resp_str = to_8_string(&resp)?; + resp_str.truncate(message.len()); + log::info!("Initial string: {resp_str}"); + Ok(()) + } + + fn read_signed_meter_values(&self) -> Result { + let length_of_values = self.read_holding_registers_fixed::<1>(7057)?[0]; + log::info!("Length of values: {}", length_of_values); + let registers_amount = (length_of_values + 1) / 2; + let regs = self.read_holding_registers(7612, registers_amount)?; + let mut json_value = to_8_string(®s)?; + json_value.truncate(length_of_values as usize); + log::info!("Read the signed meter values: {}", json_value); + Ok(json_value) + } + + fn read_signature(&self) -> Result { + let length_of_signature = self.read_holding_registers_fixed::<1>(7058)?[0]; + log::info!("Length of signature: {}", length_of_signature); + let registers_amount = (length_of_signature + 1) / 2; + let regs = self.read_holding_registers(8188, registers_amount)?; + let mut signature = to_hex_string(regs); + signature.truncate((length_of_signature * 2) as usize); + log::info!("Read the signature: {}", signature); + Ok(signature) + } + + /// For 15 seconds and checks the signature status every 10 ms + fn check_signature_status(&self) -> Result<()> { + retry::retry( + || { + let status = self.read_command_status()?; + match status { + SignatureStatus::NotInitialized + | SignatureStatus::Idle + | SignatureStatus::SignatureInProgress => Ok(retry::RetryState::KeepTrying), + SignatureStatus::SignatureOK => Ok(retry::RetryState::Done), + error => anyhow::bail!("Error state {error:?}"), + } + }, + Duration::from_secs(15), + ) + } + + fn start_transaction( + &self, + req: generated::types::powermeter::TransactionReq, + ) -> Result { + // We can start transaction only in Idle state + let state = self.read_state()?; + match state { + IskraMaterState::Active + | IskraMaterState::Active_after_power_failure + | IskraMaterState::Active_after_reset => { + log::error!("Unexpected state {state:?}, trying to stop transaction"); + self.stop_transaction()?; + } + IskraMaterState::Idle => {} + IskraMaterState::Unknown => { + log::warn!("Unknown state"); + } + } + self.set_time()?; + log::info!("Set time"); + // set algorithm + self.write_single_register(7059, 0)?; + self.write_metadata(&req.evse_id)?; + + // Finally send start transaction, we are sending 'B' + self.write_single_register(7051, 0x4200)?; + log::info!("Started transaction."); + + // Wait until the meter is active. + retry::retry( + || { + let state = self.read_state()?; + match state { + IskraMaterState::Active => Ok(retry::RetryState::Done), + _ => Ok(retry::RetryState::KeepTrying), + } + }, + std::time::Duration::from_secs(1), + )?; + + self.check_signature_status()?; + let signature = self.read_signature()?; + let signed_meter_values = self.read_signed_meter_values()?; + Ok(TransactionStartResponse { + error: Option::None, + signed_meter_value: Some(SignedMeterValue { + signed_meter_data: create_ocmf(signed_meter_values, signature), + signing_method: String::new(), + encoding_method: "OCMF".to_string(), + public_key: self.read_public_key().ok(), + timestamp: None, + }), + transaction_min_stop_time: Option::None, + status: TransactionRequestStatus::OK, + transaction_max_stop_time: Option::None, + }) + } + + fn stop_transaction(&self) -> Result { + // We can start transaction only in Idle state + let state = self.read_state()?; + match state { + IskraMaterState::Idle => { + log::error!("The state of meter is Idle and we can not stop transaction",); + anyhow::bail!("Transaction not started"); + } + IskraMaterState::Active + | IskraMaterState::Active_after_power_failure + | IskraMaterState::Active_after_reset => {} + IskraMaterState::Unknown => { + log::warn!("Unknown state") + } + } + + // The state for start transaction is incorrect + self.write_single_register(7051, 0x7200)?; + + // Wait until the meter is idle. + retry::retry( + || { + let state = self.read_state()?; + match state { + IskraMaterState::Idle => Ok(retry::RetryState::Done), + _ => Ok(retry::RetryState::KeepTrying), + } + }, + std::time::Duration::from_secs(1), + )?; + + self.check_signature_status()?; + let signature = self.read_signature()?; + let signed_meter_values = self.read_signed_meter_values()?; + Ok(TransactionStopResponse { + error: Option::None, + signed_meter_value: Some(SignedMeterValue { + signed_meter_data: create_ocmf(signed_meter_values, signature), + signing_method: String::new(), + encoding_method: "OCMF".to_string(), + public_key: self.read_public_key().ok(), + timestamp: None, + }), + status: TransactionRequestStatus::OK, + }) + } + + /// Read the public key once and cache it. + fn read_public_key(&self) -> Result { + let mut lock = self.public_key.lock().expect("Never poisoned"); + match &*lock { + Some(key) => Ok(key.clone()), + None => { + let regs = self.read_holding_registers(8124, 32)?; + + let key = format!("{}{}", PUBLIC_KEY_PREFIX, &to_hex_string(regs)); + *lock = Some(key.clone()); + Ok(key) + } + } + } +} + +/// The state machine of this module. +enum StateMachine { + InitState(InitState), + ReadyState(ReadyState), +} + +/// Main class implementing all EVerest traits. +struct IskraMeter { + state_machine: Mutex, +} + +impl generated::OnReadySubscriber for IskraMeter { + fn on_ready(&self, publishers: &generated::ModulePublisher) { + let mut lock = self.state_machine.lock().unwrap(); + let StateMachine::InitState(ref init_state) = *lock else { + log::warn!("StateMachine already initialized"); + return; + }; + + // Update from `InitState` to `ReadyState`. + let ready_state = ReadyState::new(init_state.clone(), publishers.modbus.clone()); + + fn print_spec(res: &Result, name: &str) + where + R: Debug, + E: Debug, + { + match res { + Ok(ok) => log::info!("{name}: {ok:?}"), + Err(err) => log::error!("Failed to read {name}: {err:?}"), + }; + } + print_spec(&ready_state.read_device_group(), "device group"); + print_spec(&ready_state.read_device_model(), "device model"); + print_spec(&ready_state.read_device_serial(), "device serial"); + + let ready_state_clone = ready_state.clone(); + let power_meter_clone = publishers.meter.clone(); + std::thread::spawn(move || loop { + std::thread::sleep(std::time::Duration::from_secs(5)); + let res = ready_state_clone.read_meter_value(); + match res { + Ok(meter) => { + log::info!("Got meter value {:?}", meter); + match power_meter_clone.powermeter(meter) { + Ok(_) => log::info!("Successfully published meter value"), + Err(e) => log::error!("Failed to post meter values {:?}", e), + } + } + Err(e) => log::error!("Failed to read meter value {:?}", e), + } + }); + + // Finally update the state in the lock. + *lock = StateMachine::ReadyState(ready_state); + } +} + +impl generated::SerialCommunicationHubClientSubscriber for IskraMeter {} + +impl generated::PowermeterServiceSubscriber for IskraMeter { + fn start_transaction( + &self, + _context: &generated::Context, + value: generated::types::powermeter::TransactionReq, + ) -> everestrs::Result { + let lock = self + .state_machine + .lock() + .map_err(|_| ::everestrs::Error::InvalidArgument("Internal error"))?; + + let StateMachine::ReadyState(ready_state) = &*lock else { + return Err(::everestrs::Error::InvalidArgument("Not initialized")); + }; + + let res = ready_state.start_transaction(value); + + match res { + Ok(result) => Ok(result), + Err(e) => { + log::error!("Failed to start transaction {:?}", e); + Ok(TransactionStartResponse::from_err(&e)) + } + } + } + + fn stop_transaction( + &self, + _context: &generated::Context, + _transaction_id: String, + ) -> everestrs::Result { + let lock = self + .state_machine + .lock() + .map_err(|_| ::everestrs::Error::InvalidArgument("Internal error"))?; + + let StateMachine::ReadyState(ready_state) = &*lock else { + return Err(::everestrs::Error::InvalidArgument("Not initialized")); + }; + + let res = ready_state.stop_transaction(); + + match res { + Ok(result) => Ok(result), + Err(e) => { + log::error!("Failed to stop transaction {:?}", e); + Ok(TransactionStopResponse::from_err(&e)) + } + } + } +} + +fn main() { + logger::init_logger("RS_ISKRA_METER_LOGGER_LEVEL"); + let config = get_config(); + let class = Arc::new(IskraMeter { + state_machine: Mutex::new(StateMachine::InitState(InitState::new( + config.powermeter_device_id, + (&config).into(), + ))), + }); + + let _module = Module::new(class.clone(), class.clone(), class.clone()); + + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } +} + +#[cfg(test)] +mod tests { + + use self::generated::types::powermeter::TransactionReq; + + use super::*; + use mockall::predicate::eq; + + /// Helper to produce the class under test. + fn make_ready_state(publisher: SerialCommunicationHubClientPublisher) -> ReadyState { + ReadyState::new(InitState::new(1234, OcmfData::default()), publisher) + } + + #[test] + fn serial_comm_hub_requests__Result__conversion() { + use generated::types::serial_comm_hub_requests::Result; + + // Test with invalid input + let error_input = [ + Result { + status_code: StatusCodeEnum::Error, + value: None, + }, + Result { + status_code: StatusCodeEnum::Success, + value: None, + }, + Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![1, 2, 3]), + }, + ]; + + for input in error_input { + assert!(Result::into_vec(input.clone(), 2).is_err()); + assert!(>>::into(input).is_err()); + } + + // Test with valid input. + let correct_input = [ + ( + Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![]), + }, + vec![], + ), + ( + Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![1, 2, 3]), + }, + vec![1, 2, 3], + ), + ]; + + for (input, expected) in correct_input.iter() { + let output = Result::into_vec(input.clone(), expected.len()).unwrap(); + assert_eq!(output, *expected); + } + + // Test for arrays. + let output: [u16; 0] = + >>::into(correct_input[0].0.clone()).unwrap(); + assert_eq!(output.len(), 0); + + let output: [u16; 3] = + >>::into(correct_input[1].0.clone()).unwrap(); + assert_eq!(output, [1, 2, 3]); + } + + #[test] + fn retry__retry() { + // Test the fail case without retrying + let res = retry::retry(|| anyhow::bail!("Failure"), Duration::from_secs(1)); + assert!(res.is_err()); + + // Test the timeout case + let res = retry::retry( + || Ok(retry::RetryState::KeepTrying), + Duration::from_millis(5), + ); + assert!(res.is_err()); + + // Test the success case without retry + let res = retry::retry(|| Ok(retry::RetryState::Done), Duration::from_secs(1)); + assert!(res.is_ok()); + + // Test the success after retry. + let counter = Mutex::new(0); + let res = retry::retry( + || { + if *counter.lock().unwrap() == 1 { + return Ok(retry::RetryState::Done); + } + *counter.lock().unwrap() += 1; + Ok(retry::RetryState::KeepTrying) + }, + Duration::from_secs(1), + ); + assert!(res.is_ok()); + } + + #[test] + fn ready_state__write_metadata() { + let mut mock = SerialCommunicationHubClientPublisher::default(); + + mock.expect_modbus_write_multiple_registers() + .times(1) + .returning(|_, _, _| Ok(StatusCodeEnum::Success)); + + mock.expect_modbus_write_single_register() + .with(eq(189), eq(7056), eq(1234)) + .times(1) + .returning(|_, _, _| Ok(StatusCodeEnum::Success)); + + mock.expect_modbus_read_holding_registers() + .with(eq(7100), eq(95), eq(1234)) + .times(1) + .returning(|_, _, _| { + Ok(generated::types::serial_comm_hub_requests::Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![u16::from_be_bytes([b' ', b' ']) as i64; 95]), + }) + }); + + let ready_state = make_ready_state(mock); + ready_state.write_metadata("some evse id").unwrap(); + } + + #[test] + fn ready_state__read_signed_meter_values() { + let mut mock = SerialCommunicationHubClientPublisher::default(); + + mock.expect_modbus_read_holding_registers() + .with(eq(7057), eq(1), eq(1234)) + .times(1) + .returning(|_, _, _| { + Ok(generated::types::serial_comm_hub_requests::Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![15]), + }) + }); + + mock.expect_modbus_read_holding_registers() + .with(eq(7612), eq(8), eq(1234)) + .times(1) + .returning(|_, _, _| { + Ok(generated::types::serial_comm_hub_requests::Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![u16::from_be_bytes([b'a', b'b']) as i64; 8]), + }) + }); + + let ready_state = make_ready_state(mock); + let res = ready_state.read_signed_meter_values().unwrap(); + assert_eq!(res, "abababababababa"); + } + + #[test] + fn ready_state__read_signature() { + let mut mock = SerialCommunicationHubClientPublisher::default(); + + mock.expect_modbus_read_holding_registers() + .with(eq(7058), eq(1), eq(1234)) + .times(1) + .returning(|_, _, _| { + Ok(generated::types::serial_comm_hub_requests::Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![9]), + }) + }); + + mock.expect_modbus_read_holding_registers() + .with(eq(8188), eq(5), eq(1234)) + .times(1) + .returning(|_, _, _| { + Ok(generated::types::serial_comm_hub_requests::Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![0xdead, 0xbeef, 0xabcd, 0x1234, 0x5678]), + }) + }); + + let ready_state = make_ready_state(mock); + let res = ready_state.read_signature().unwrap(); + assert_eq!(res, "DEADBEEFABCD123456"); + } + + #[test] + fn ready_state__check_signature_status() { + let parameters = [(15, true), (16, false)]; + for (input, expected) in parameters { + let mut mock = SerialCommunicationHubClientPublisher::default(); + mock.expect_modbus_read_holding_registers() + .with(eq(7052), eq(1), eq(1234)) + .times(1) + .returning(move |_, _, _| { + Ok(generated::types::serial_comm_hub_requests::Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![input as i64]), + }) + }); + + let ready_state = make_ready_state(mock); + assert_eq!(ready_state.check_signature_status().is_ok(), expected); + } + } + + #[test] + /// Test verifies that when we try to start the transaction and the meter + /// is already running a transaction, that we stop the ongoing transaction + /// before proceeding. + fn ready_state__start_transaction__try_stop() { + // The values correspond to the three `Active` values of Iskra. + for value in [1, 2, 3] { + let mut mock = SerialCommunicationHubClientPublisher::default(); + // We expect that this is called twice - once in the start_transaction + // and once in the stop_transaction. + mock.expect_modbus_read_holding_registers() + .with(eq(7000), eq(1), eq(1234)) + .times(2) + .returning(move |_, _, _| { + Ok(generated::types::serial_comm_hub_requests::Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![value as i64]), + }) + }); + + // This is the call to stop the transaction. We return error to abort + // the further sequence. + mock.expect_modbus_write_single_register() + .with(eq(0x7200), eq(7051), eq(1234)) + .returning(|_, _, _| Ok(StatusCodeEnum::Error)); + + let ready_state = make_ready_state(mock); + + let _unused = ready_state.start_transaction(TransactionReq { + cable_id: 1, + client_id: String::new(), + evse_id: String::new(), + tariff_id: 1, + transaction_id: String::new(), + user_data: String::new(), + }); + } + } + + #[test] + fn ready_state__read_public_key() { + let mut mock = SerialCommunicationHubClientPublisher::default(); + // We expect times(1) since the other calls should be cached. + mock.expect_modbus_read_holding_registers() + .with(eq(8124), eq(32), eq(1234)) + .times(1) + .returning(move |_, _, _| { + Ok(generated::types::serial_comm_hub_requests::Result { + status_code: StatusCodeEnum::Success, + value: Some(vec![0; 32]), + }) + }); + + let ready_state = make_ready_state(mock); + let expected = "3059301306072A8648CE3D020106082A8648CE3D0301070342000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string(); + for _ in 0..2 { + assert_eq!(ready_state.read_public_key().unwrap(), expected); + } + } +} diff --git a/modules/RsIskraMeter/src/utils.rs b/modules/RsIskraMeter/src/utils.rs new file mode 100644 index 000000000..1123220e3 --- /dev/null +++ b/modules/RsIskraMeter/src/utils.rs @@ -0,0 +1,166 @@ +use anyhow::Result; +use rand::Rng; + +pub fn to_8_string(input: &[u16]) -> Result { + let u8_slice = input.iter().flat_map(|&x| u16::to_be_bytes(x)).collect(); + Ok(String::from_utf8(u8_slice)?.trim().to_string()) +} + +pub fn counter(regs: [u16; 2], exp: u16) -> f64 { + let measurement = (regs[0] as i32) << 16 | ((regs[1] as i32) & 0xFFFF); + measurement as f64 * 1.0e1_f64.powi(exp as i32) +} + +/// Unsigned Measurement (32 bit) +/// Decade Exponent (Signed 8 bit) +/// Binary Signed value (24 bit) +pub fn from_t5_format(regs: [u16; 2]) -> f64 { + let exp = ((regs[0] >> 8) & 0xFF) as i8; + let value = (regs[0] & 0xFF) as u8; + let measurement = ((value as u32) << 16) | ((regs[1] as u32) & 0xFFFF); + measurement as f64 * 1.0e1_f64.powi(exp as i32) +} + +/// Signed Measurement (32 bit) +/// Decade Exponent (Signed 8 bit) +/// Binary Signed value (24 bit) +pub fn from_t6_format(regs: [u16; 2]) -> f64 { + let exp = ((regs[0] >> 8) & 0xFF) as i8; + let value = (regs[0] & 0xFF) as i8; + let measurement = ((value as i32) << 16) | ((regs[1] as i32) & 0xFFFF); + measurement as f64 * 1.0e1_f64.powi(exp as i32) +} + +pub fn string_to_vec(input: &str) -> Vec { + input + .as_bytes() + .chunks(2) + .map(|chunk| { + let mut value = (chunk[0] as u16) << 8; + if chunk.len() > 1 { + value |= chunk[1] as u16 & 0xFF; + } + value + }) + .collect() +} + +pub fn create_random_meter_session_id() -> String { + let start = format!("{:06X}", rand::thread_rng().gen_range(0..=0xFFFFFF)); + let hex_time = format!( + "{:X}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let end = format!("{:06X}", rand::thread_rng().gen_range(0..=0xFFFFFF)); + + start + &hex_time + &end +} + +pub fn to_hex_string(input: Vec) -> String { + input + .into_iter() + .flat_map(|value| value.to_be_bytes()) + .map(|value| format!("{value:02X}")) + .collect::>() + .concat() +} + +pub fn create_ocmf(signed_meter_values: String, signature: String) -> String { + format!("OCMF|{}|{{\"SD\":\"{}\"}}", signed_meter_values, signature) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + /// Tests for the `to_8_string` conversion. + fn test__to_8_string() { + let parameter = [ + (vec![], ""), + ( + vec![ + u16::from_be_bytes([b' ', b'\r']), + u16::from_be_bytes([b'h', b'e']), + u16::from_be_bytes([b'l', b'l']), + u16::from_be_bytes([b'o', b' ']), + ], + "hello", + ), + (vec![u16::from_be_bytes([b' ', b'a']); 5], "a a a a a"), + (vec![u16::from_be_bytes([b' ', b' ']); 5], ""), + ]; + + for (input, expected) in parameter { + let output = to_8_string(&input).unwrap(); + assert_eq!(&output, expected); + } + } + + #[test] + /// Tests the `counter` conversion. + fn test__counter() { + let parameters = [ + ([0, 0], 1, 0.0), + ([0, 1234], 0, 1234.0), + ([0, 1234], 1, 12340.0), + ([16, 0], 0, (16 << 16) as f64), + ]; + for (input_reg, input_exp, expected) in parameters { + assert_eq!(counter(input_reg, input_exp), expected); + } + } + + #[test] + /// Tests the `from_t5_format` and `from_t6_format` conversionss. + fn test__from_tx_format() { + let parameters = [ + ([0, 0], 0.0), + ([0, 1234], 1234.0), + ([u16::from_be_bytes([3, 2]), 0], (2 << 16) as f64 * 1000.0), + ]; + + for (input, expected) in parameters { + assert_eq!(from_t5_format(input), expected); + assert_eq!(from_t6_format(input), expected); + } + } + + #[test] + fn test__string_to_vec() { + let parameters = [ + ("", vec![]), + ( + "hello", + vec![ + u16::from_be_bytes([b'h', b'e']), + u16::from_be_bytes([b'l', b'l']), + (b'o' as u16) << 8, + ], + ), + ( + "test", + vec![ + u16::from_be_bytes([b't', b'e']), + u16::from_be_bytes([b's', b't']), + ], + ), + ]; + + for (input, expected) in parameters { + assert_eq!(string_to_vec(input), expected); + } + } + + #[test] + fn test__to_hex_string() { + let parameters = [(vec![], ""), (vec![0xdead, 0xbeef], "DEADBEEF")]; + + for (input, expected) in parameters { + assert_eq!(to_hex_string(input), expected); + } + } +} diff --git a/modules/rust_examples/Cargo.lock b/modules/rust_examples/Cargo.lock deleted file mode 100644 index 1ae93386d..000000000 --- a/modules/rust_examples/Cargo.lock +++ /dev/null @@ -1,310 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "RsExample" -version = "0.1.0" -dependencies = [ - "everestrs", - "everestrs-build", - "serde", - "serde_json", -] - -[[package]] -name = "RsExampleUser" -version = "0.1.0" -dependencies = [ - "everestrs", - "everestrs-build", - "serde", - "serde_json", -] - -[[package]] -name = "anyhow" -version = "1.0.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" - -[[package]] -name = "argh" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219" -dependencies = [ - "argh_derive", - "argh_shared", -] - -[[package]] -name = "argh_derive" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a" -dependencies = [ - "argh_shared", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "argh_shared" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531" -dependencies = [ - "serde", -] - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cxx" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "everestrs" -version = "0.1.0" -source = "git+https://github.com/everest/everest-framework.git?rev=21b5371ec38158ddfcbe9eea0f92a18154ca3c76#21b5371ec38158ddfcbe9eea0f92a18154ca3c76" -dependencies = [ - "argh", - "cxx", - "everestrs-build", - "log", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "everestrs-build" -version = "0.1.0" -source = "git+https://github.com/everest/everest-framework.git?rev=21b5371ec38158ddfcbe9eea0f92a18154ca3c76#21b5371ec38158ddfcbe9eea0f92a18154ca3c76" -dependencies = [ - "anyhow", - "argh", - "convert_case", - "minijinja", - "serde", - "serde_json", - "serde_yaml", -] - -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" - -[[package]] -name = "indexmap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "libc" -version = "0.2.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" - -[[package]] -name = "link-cplusplus" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" -dependencies = [ - "cc", -] - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "minijinja" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "208758577ef2c86cf5dd3e85730d161413ec3284e2d73b2ef65d9a24d9971bcb" -dependencies = [ - "serde", -] - -[[package]] -name = "proc-macro2" -version = "1.0.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "serde" -version = "1.0.193" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.193" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "syn" -version = "2.0.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" diff --git a/modules/rust_examples/Cargo.toml b/modules/rust_examples/Cargo.toml deleted file mode 100644 index 9381e5e7e..000000000 --- a/modules/rust_examples/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[workspace] -resolver = "2" -members = [ - "RsExample", - "RsExampleUser", -] - -[workspace.dependencies] -everestrs = { git = "https://github.com/everest/everest-framework.git", rev = "21b5371ec38158ddfcbe9eea0f92a18154ca3c76" } -everestrs-build = { git = "https://github.com/everest/everest-framework.git", rev = "21b5371ec38158ddfcbe9eea0f92a18154ca3c76" } diff --git a/third-party/bazel/repos.bzl b/third-party/bazel/repos.bzl index 22953512f..5053e0b1d 100644 --- a/third-party/bazel/repos.bzl +++ b/third-party/bazel/repos.bzl @@ -13,6 +13,6 @@ def everest_core_repos(): maybe( http_archive, name = "everest-framework", - url = "https://github.com/everest/everest-framework/archive/21b5371ec38158ddfcbe9eea0f92a18154ca3c76.tar.gz", - strip_prefix = "everest-framework-21b5371ec38158ddfcbe9eea0f92a18154ca3c76", + url = "https://github.com/everest/everest-framework/archive/01468678f5b627a96791f749f3139c3d169bf081.tar.gz", + strip_prefix = "everest-framework-01468678f5b627a96791f749f3139c3d169bf081", ) diff --git a/types/powermeter.yaml b/types/powermeter.yaml index b3acb1b57..4266eaaf6 100644 --- a/types/powermeter.yaml +++ b/types/powermeter.yaml @@ -45,11 +45,16 @@ types: - status properties: status: + type: object description: Response status that indicates whether the transaction start request could successfully be performed. $ref: /powermeter#/TransactionRequestStatus error: description: If status is not OK, a verbose error message. type: string + signed_meter_value: + description: The signed meter value report of the started transaction. Must be provided if status is OK. + type: object + $ref: /units_signed#/SignedMeterValue transaction_min_stop_time: description: Earliest point in time the started transaction can be stopped again (if a minimum duration is required by the meter); yields a RFC3339 timestamp. type: string @@ -66,6 +71,7 @@ types: - status properties: status: + type: object description: Response status that indicates whether the transaction stop request could successfully be performed. $ref: /powermeter#/TransactionRequestStatus signed_meter_value: