From 20ae5443e5e6626fd4bda12131d9e496b9faf34b Mon Sep 17 00:00:00 2001 From: Charlie B <11893617+qubitter@users.noreply.github.com> Date: Wed, 10 Jan 2024 09:53:14 -0500 Subject: [PATCH] Translator (#26) * cleanup from old branch * added little structs * added big structs * added overarching objects * reworked mapping to use tags * added new PoC * updated module paths to make mypy slightly happier --- .gitignore | 20 ++++++++---- .vscode/settings.json | 3 -- oxy/RustSynth.py | 42 +++++++++++++++++++++++++ oxy/YAMLParser.py | 25 +++++++++++++++ oxy/mapping.yaml | 54 +++++++++++++++++++++++++++++++++ oxy/poc_translator.py | 48 +++++++++++++++++++++++++++++ oxy/structs/CANField.py | 20 ++++++++++++ oxy/structs/CANmsg.py | 24 +++++++++++++++ oxy/structs/CorrectingFactor.py | 9 ++++++ oxy/structs/Decoding.py | 23 ++++++++++++++ oxy/typedpoc.py | 4 +++ src/master_mapping.rs | 2 +- 12 files changed, 264 insertions(+), 10 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 oxy/RustSynth.py create mode 100644 oxy/YAMLParser.py create mode 100644 oxy/mapping.yaml create mode 100644 oxy/poc_translator.py create mode 100644 oxy/structs/CANField.py create mode 100644 oxy/structs/CANmsg.py create mode 100644 oxy/structs/CorrectingFactor.py create mode 100644 oxy/structs/Decoding.py create mode 100644 oxy/typedpoc.py diff --git a/.gitignore b/.gitignore index 0371fe5..15f2f6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,18 @@ -build/ -dist/ -__pycache__/ -logs/ +# editor things .idea/ -output.csv +.zed/ +.vscode/ -# Added by cargo +# misc things +.DS_Store +.gitignore +# python things +pyrightconfig.json +__pycache__/ + +# Added by cargo (rust things) /target +build/ +dist/ +logs/ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 60bf230..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "editor.defaultFormatter": "rust-lang.rust-analyzer" -} diff --git a/oxy/RustSynth.py b/oxy/RustSynth.py new file mode 100644 index 0000000..c332f4c --- /dev/null +++ b/oxy/RustSynth.py @@ -0,0 +1,42 @@ +from structs.CANField import CANField +from structs.CANMsg import CANMsg +from structs.Decoding import Decoding + +from typing import Optional + +class RustSynth: + ''' + A class to synthesize Rust from a given CANMsg spec. + ''' + + inst_hashmap: str = " let mut result = HashMap::new();" + closing: str = " result\n}" + + def synthesize(self, msg: CANMsg) -> str: + signature: str = self.signature(msg.desc) + generated_lines: list[str] = [] + for field in msg.fields: + generated_lines.append(self.finalize_line(field.id, f"({self.parse_decoders(field)}){self.correcting_factor(field)}")) + total_list: list[str] = [signature, self.inst_hashmap] + generated_lines + [self.closing] + return "\n".join(total_list) + + def signature(self, to_decode: str) -> str: + return f"pub fn decode_{to_decode.replace(' ', '_')}(data: &[u8]) -> HashMap {{" + + def finalize_line(self, id: int, val: str) -> str: + return f" result.insert({id}, {val});" + + def parse_decoders(self, field: CANField) -> str: + if isinstance(field.decodings, type(None)): + return f"data[{field.index}] as f32" + else: + base: str = f"&data[{field.index}..{field.index + field.size}]" + for decoder in field.decodings: + base = f"pd::{decoder.repr}({base}, {decoder.bits}) as {decoder.final_type}" + return base + + def correcting_factor(self, field:CANField) -> str: + cf: str = "" + if field.correcting_factor: + cf = f" {field.correcting_factor.op} {field.correcting_factor.const}" + return cf diff --git a/oxy/YAMLParser.py b/oxy/YAMLParser.py new file mode 100644 index 0000000..72a935f --- /dev/null +++ b/oxy/YAMLParser.py @@ -0,0 +1,25 @@ +from io import TextIOWrapper +from ruamel.yaml import YAML, Any + +from structs.CANMsg import CANMsg +from structs.CANField import CANField +from structs.CorrectingFactor import CorrectingFactor +import structs.Decoding + +class YAMLParser: + ''' + A class to parse a given YAML string or file. Most of the heavy lifting + is done by the internals of ruamel.yaml. + ''' + + def __init__(self): + self.yaml = YAML() + self.yaml.register_class(CANMsg) + self.yaml.register_class(CANField) + self.yaml.register_class(CorrectingFactor) + for decoding in structs.Decoding.Decoding.__subclasses__(): + self.yaml.register_class(decoding) + + + def parse(self, file: Any) -> CANMsg: + return self.yaml.load(file) diff --git a/oxy/mapping.yaml b/oxy/mapping.yaml new file mode 100644 index 0000000..7182e03 --- /dev/null +++ b/oxy/mapping.yaml @@ -0,0 +1,54 @@ +!CANMsg +id: 1 +desc: "accumulator status" +fields: +- !CANField + id: 1 + name: Pack Inst Voltage + units: "V" + size: 2 + decodings: + - !BigEndian + bits: 8 + final_type: "f32" + correcting_factor: + !CorrectingFactor + const: 10.0 + op: "/" +- !CANField + id: 2 + name: "Pack Current" + units: "A" + size: 2 + decodings: + - !BigEndian + bits: 8 + final_type: "u32" + - !TwosComplement + bits: 16 + final_type: "f32" + correcting_factor: + !CorrectingFactor + const: 10.0 + op: "/" +- !CANField + id: 3 + name: "Pack Amp-hours" + units: "Ah" + size: 2 + decodings: + - !BigEndian + bits: 8 + final_type: "f32" +- !CANField + id: 4 + name: "Pack SOC" + units: "%" + size: 1 + final_type: "f32" +- !CANField + id: 5 + name: "Pack Health" + units: "%" + size: 1 + final_type: "f32" diff --git a/oxy/poc_translator.py b/oxy/poc_translator.py new file mode 100644 index 0000000..51c2db1 --- /dev/null +++ b/oxy/poc_translator.py @@ -0,0 +1,48 @@ +from ruamel.yaml import YAML, Any +from functools import reduce + + +yaml: YAML = YAML(typ="safe") + +out_string: str = "" +data: dict[str, Any] = yaml.load(open("mapping.yaml")) +print(data) +print(type(data)) + +function_name: str = "decode" + "_" + "_".join(data['string'].split(" ")) +args: str = "(data: &[u8])" +returnVal: str= " -> HashMap" + +signature: str = "pub fn " + function_name + args + returnVal + " {" +instantiate_hash_map: str = " let mut result = HashMap::new();" +conclusion: str = " result\n}" + +decodings: list[str] = [] +accumulated_size: int = 0 +for field in data["fields"]: # result.insert(1, (pd::big_endian(&data[0..2], 8) as f32) / 10.0); + field: dict + decoded: str + id = field["field_id"] + if field["size"] > 1: # we need to do some decoding, then + to_decode: str = f"&data[{accumulated_size}..{accumulated_size+field['size']}]" + _cf: str = field.get("correcting_factor", "") + correcting_factor: str = f"{' ' + ('/' if '/' in _cf else '*') + ' ' if 'correcting_factor' in field.keys() else ''}{_cf.split('/')[-1]}" + for decodingsetup in field["decoding"]: + decodingsetup: dict[str, dict[str, str]] = {k: reduce(lambda x,y: x|y, v, {}) for k,v in decodingsetup.items()} + for decoder, params in decodingsetup.items(): + match decoder: + case "big_endian": + to_decode = f"pd::big_endian({to_decode}, {params['bits']}) as {params['final_type']}" + case "twos_complement": + to_decode = f"pd::twos_comp({to_decode}, {params['bits']}) as {params['final_type']}" + decoded = f"{id}, {to_decode}{correcting_factor}" + else: # no decoding required! + decoded = f"{id}, data[{accumulated_size}] as {field['final_type']}" + + decodings.append(decoded) + accumulated_size += field["size"] + +formatted_decodings = [f" result.insert({i});" for i in decodings] + +finals: list[str] = [signature, instantiate_hash_map] + formatted_decodings + [conclusion] +print("\n".join(finals)) diff --git a/oxy/structs/CANField.py b/oxy/structs/CANField.py new file mode 100644 index 0000000..bf32677 --- /dev/null +++ b/oxy/structs/CANField.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from .CorrectingFactor import CorrectingFactor +from .Decoding import * +from ruamel.yaml import Optional +from dataclasses import dataclass + +@dataclass +class CANField: + ''' + Represents a field in a CAN message. Has an id, a name, units, a size, + and an optional CorrectingFactor and Decodings. Also knows its own + index within its parent CANMsg, which is assigned at load from YAML. + ''' + id: int + name: str + units: str + size: int + index: int = -1 + correcting_factor: Optional[CorrectingFactor] = None + decodings: Optional[list[Decoding]] = None diff --git a/oxy/structs/CANmsg.py b/oxy/structs/CANmsg.py new file mode 100644 index 0000000..d35664e --- /dev/null +++ b/oxy/structs/CANmsg.py @@ -0,0 +1,24 @@ +from __future__ import annotations +from ruamel.yaml import Optional, MappingNode +from structs.CorrectingFactor import CorrectingFactor +from .CANField import CANField +from dataclasses import dataclass + +@dataclass +class CANMsg: + ''' + Represents a CAN message. Has an id, a description, and a number of individual fields. + ''' + id: int + desc: str + fields: list[CANField] + + def __post_init__(self) -> None: + idx: int = 0 + for field in self.fields: + field.index = idx + idx += field.size + + + def __setstate__(self, state): + self.__init__(**state) diff --git a/oxy/structs/CorrectingFactor.py b/oxy/structs/CorrectingFactor.py new file mode 100644 index 0000000..530354c --- /dev/null +++ b/oxy/structs/CorrectingFactor.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + +@dataclass +class CorrectingFactor: + ''' + Represents a correcting factor to be applied to data after decoding. + ''' + const: float + op: str diff --git a/oxy/structs/Decoding.py b/oxy/structs/Decoding.py new file mode 100644 index 0000000..e461fbe --- /dev/null +++ b/oxy/structs/Decoding.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass + +@dataclass +class Decoding: + ''' + This is an abstract class (well, what passes for one in Python) + that represents a decoding to be applied to a slice of data. + ''' + bits: int + final_type: str + repr: str = "*"*42 + +@dataclass +class BigEndian(Decoding): + repr: str = "big_endian" + +@dataclass +class LittleEndian(Decoding): + repr: str = "little_endian" + +@dataclass +class TwosComplement(Decoding): + repr: str = "twos_comp" diff --git a/oxy/typedpoc.py b/oxy/typedpoc.py new file mode 100644 index 0000000..89abd50 --- /dev/null +++ b/oxy/typedpoc.py @@ -0,0 +1,4 @@ +from YAMLParser import YAMLParser +from RustSynth import RustSynth + +print(RustSynth().synthesize(YAMLParser().parse(open("mapping.yaml", "r")))) diff --git a/src/master_mapping.rs b/src/master_mapping.rs index 5441bbe..d5d7fa1 100644 --- a/src/master_mapping.rs +++ b/src/master_mapping.rs @@ -88,6 +88,7 @@ impl DataInfo { } } +// maps from data id to DataInfo containing the name of the data and its units pub fn get_data_info(id: u8) -> DataInfo { match id { 0 => return DataInfo::new("Mock Data".to_string(), "".to_string()), @@ -242,6 +243,5 @@ pub fn get_data_info(id: u8) -> DataInfo { 145 => return DataInfo::new("Precharge State".to_string(), "".to_string()), 146 => return DataInfo::new("BMS Prefault Status".to_string(), "".to_string()), _ => return DataInfo::new("".to_string(), "".to_string()), - } }