Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
|
||||
load("@everest_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//errors",
|
||||
"@everest-core//interfaces",
|
||||
"@everest-core//types",
|
||||
],
|
||||
deps = all_crate_deps(build = True) + [
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
],
|
||||
)
|
||||
|
||||
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",
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
|
||||
"//lib/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"],
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "RsIskraMeter"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[build-dependencies]
|
||||
everestrs-build = { workspace=true }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
backon = "1.2.0"
|
||||
chrono = "0.4.31"
|
||||
everestrs = { workspace=true }
|
||||
log = "0.4.20"
|
||||
rand = "0.8.5"
|
||||
mockall = { version = "0.12.1", optional = true }
|
||||
mockall_double = { version = "0.3.1", optional = true}
|
||||
|
||||
[features]
|
||||
default = ["mockall", "mockall_double"]
|
||||
@@ -0,0 +1,65 @@
|
||||
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. If you set it to `EVSEID`
|
||||
it will be overwritten by the evse-id provided by `TransactionReq`
|
||||
default: ""
|
||||
type: string
|
||||
communication_errors_threshold:
|
||||
description: The maximum number of consecutive errors allowed before a persistent error is reported
|
||||
default: 10
|
||||
type: integer
|
||||
lcd_main_text:
|
||||
description: Text to display when there is no transaction. At most 8 chars.
|
||||
type: string
|
||||
default: ""
|
||||
lcd_label_text:
|
||||
description: Label to display when there is no transaction. At most 4 chars.
|
||||
type: string
|
||||
default: ""
|
||||
read_meter_values_interval_ms:
|
||||
description: How often to read the meter values in milliseconds. The refresh rate of WM3M4C is ~1 second.
|
||||
type: integer
|
||||
default: 5000
|
||||
minimum: 1000
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,350 @@
|
||||
use anyhow::Result;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn to_8_string(input: &[u16]) -> Result<String> {
|
||||
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<u16> {
|
||||
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<u16>) -> String {
|
||||
input
|
||||
.into_iter()
|
||||
.flat_map(|value| value.to_be_bytes())
|
||||
.map(|value| format!("{value:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.concat()
|
||||
}
|
||||
|
||||
/// The Iskra firmware has a bug where we must remove leading zeros from the r
|
||||
/// and s segments if the first non-zero byte is smaller than 0x80.
|
||||
///
|
||||
/// If we remove any zeros we have to correct the lengths of the entire signature
|
||||
/// and the affected r/s components.
|
||||
///
|
||||
/// The signature looks like
|
||||
/// 0x30, 0x44, 0x02, 0x20 ... 0x02, 0x20, ...
|
||||
/// | | | | | | length of the r/s component
|
||||
/// | | | | | start of the r/s component
|
||||
/// | | | | length of the r/s component.
|
||||
/// | | | start of r/s component.
|
||||
/// | | signature length
|
||||
/// | start
|
||||
pub fn to_signature(input: Vec<u16>) -> String {
|
||||
// The words contain two bytes - split them into bytes.
|
||||
let input: Vec<u8> = input
|
||||
.iter()
|
||||
.flat_map(|w| w.to_be_bytes().to_vec())
|
||||
.map(|c| c as u8)
|
||||
.collect();
|
||||
|
||||
fn to_hex_string_from_bytes(bytes: &[u8]) -> String {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|value| format!("{value:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.concat()
|
||||
}
|
||||
|
||||
// Check if we have the first two bytes.
|
||||
if input.len() < 2 {
|
||||
log::warn!("Signature too short, aborting");
|
||||
return to_hex_string_from_bytes(&input);
|
||||
}
|
||||
|
||||
// Check if the first element is 0x30
|
||||
if input[0] != 0x30 {
|
||||
log::warn!(
|
||||
"Signature starts with {} instead of 0x30, aborting",
|
||||
input[0]
|
||||
);
|
||||
return to_hex_string_from_bytes(&input);
|
||||
}
|
||||
|
||||
// Check the integrity of the signature
|
||||
if input.len() != input[1] as usize + 2 {
|
||||
log::warn!(
|
||||
"Signature length mismatch: Expected {} received {}, aborting",
|
||||
input.len(),
|
||||
input[1] as usize + 2
|
||||
);
|
||||
return to_hex_string_from_bytes(&input);
|
||||
}
|
||||
|
||||
let mut output = Vec::new();
|
||||
output.reserve(input.len());
|
||||
output.extend_from_slice(&input[0..2]);
|
||||
|
||||
let mut current_pos = 2;
|
||||
|
||||
// Process sub-components
|
||||
while current_pos < input.len() {
|
||||
if input[current_pos] != 2 {
|
||||
log::warn!(
|
||||
"Sub-component starts with {} instead of 2, aborting",
|
||||
input[current_pos]
|
||||
);
|
||||
return to_hex_string_from_bytes(&input);
|
||||
}
|
||||
output.push(2);
|
||||
current_pos += 1;
|
||||
|
||||
if current_pos >= input.len() {
|
||||
log::warn!("No length element, aborting");
|
||||
return to_hex_string_from_bytes(&input);
|
||||
}
|
||||
|
||||
// Get the length of the current sub-component
|
||||
let sub_component_length = input[current_pos] as usize;
|
||||
if sub_component_length == 0 {
|
||||
log::warn!("Sub-component length is 0");
|
||||
return to_hex_string_from_bytes(&input);
|
||||
}
|
||||
current_pos += 1;
|
||||
|
||||
// Check if we have enough u16 values for the sub-component
|
||||
if current_pos + sub_component_length > input.len() {
|
||||
log::warn!("Sub-component length exceeds available u16 values, aborting");
|
||||
return to_hex_string_from_bytes(&input);
|
||||
}
|
||||
|
||||
// Extract the sub-component
|
||||
let mut sub_component = &input[current_pos..current_pos + sub_component_length];
|
||||
|
||||
// Process sub-component. Find the first non-zero value.
|
||||
let non_zero_idx = sub_component
|
||||
.iter()
|
||||
.position(|&value| value != 0)
|
||||
.unwrap_or(sub_component.len() - 1);
|
||||
let non_zero_value = sub_component[non_zero_idx];
|
||||
// If there is a non-zero value, check if it is less than 0x80.
|
||||
if non_zero_value < 0x80 {
|
||||
log::info!("Removed {} leading zeros from sub-component", non_zero_idx);
|
||||
sub_component = &sub_component[non_zero_idx..];
|
||||
}
|
||||
|
||||
output.push(sub_component.len() as u8);
|
||||
output.extend_from_slice(&sub_component);
|
||||
|
||||
current_pos += sub_component_length;
|
||||
}
|
||||
|
||||
// Update the signature length (2nd u8) if any zeros were removed
|
||||
output[1] = (output.len() - 2) as u8;
|
||||
to_hex_string_from_bytes(&output)
|
||||
}
|
||||
|
||||
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"),
|
||||
(vec![0xdead, 0xbe], "DEAD00BE"),
|
||||
];
|
||||
|
||||
for (input, expected) in parameters {
|
||||
assert_eq!(to_hex_string(input), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test__correct_signature() {
|
||||
fn hex_string_to_vec(hex: &str) -> Vec<u16> {
|
||||
hex.as_bytes()
|
||||
.chunks(4)
|
||||
.map(|chunk| {
|
||||
let hex_str = std::str::from_utf8(chunk).unwrap();
|
||||
u16::from_str_radix(hex_str, 16).unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
// Test case with specific hex strings
|
||||
for (input_hex, expected_hex) in [
|
||||
// No zeros.
|
||||
("30440220015d226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce402206a9b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49",
|
||||
"30440220015d226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce402206a9b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49"),
|
||||
// In the first segment 1 byte with zero.
|
||||
("30440220005d226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce402206a9b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49",
|
||||
"3043021f5d226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce402206a9b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49"),
|
||||
// In the first segment 2 bytes with zero.
|
||||
("304402200000226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce402206a9b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49",
|
||||
"3042021e226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce402206a9b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49"),
|
||||
// In the first segment 1 bytes with zero but second element is valid
|
||||
("304402200080226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce402206a9b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49",
|
||||
"304402200080226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce402206a9b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49"),
|
||||
// In the second segment 3 bytes with zero.
|
||||
("304402200080226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce4022000000016da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49",
|
||||
"304102200080226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce4021d16da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49"),
|
||||
// In the first segment all zeros
|
||||
("30440220000000000000000000000000000000000000000000000000000000000000000002206a9b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49",
|
||||
"302502010002206a9b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49"),
|
||||
// In both are zeros.
|
||||
("30440220005d226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce40220007b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49",
|
||||
"3042021f5d226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce4021f7b5ad6da8a3234e5df67fbddd2dd98b454c74625f9340a2e82fc81c4b41d49"),
|
||||
// No second sub-component
|
||||
("30220220005d226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce4",
|
||||
"3021021f5d226c5f20ebebfd9f0d1da2aa696dd4f77f3708eecde72b4a346ae03b8ce4"),
|
||||
// The reference case used for the negative tests below. This makes
|
||||
// sure that we will modify the input if it would not contain errors.
|
||||
("300402020001", "3003020101"),
|
||||
] {
|
||||
// Convert hex strings to Vec<u16>
|
||||
let input = hex_string_to_vec(input_hex);
|
||||
// let expected = hex_string_to_vec(expected_hex);
|
||||
|
||||
assert_eq!(to_signature(input).to_uppercase(), expected_hex.to_uppercase(), "{}", input_hex);
|
||||
}
|
||||
|
||||
// Test cases with invalid input
|
||||
for input in [
|
||||
"300302020001", // Not the right length overall. 3 is Wrong.
|
||||
"310402020001", // Wrong start.
|
||||
"300403020001", // Sub-component not started by 2. 3 is Wrong.
|
||||
"300402030001", // Sub-component wrong length (too short) - should be 2.
|
||||
"300402000001", // Sub-component wrong length (zeros) - should be 2.
|
||||
"300402050001", // Sub-component wrong length (too long) - should be 2.
|
||||
] {
|
||||
let expected = hex_string_to_vec(input);
|
||||
assert_eq!(to_signature(expected.clone()), input);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user