diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..aee1069 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: Tests +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: "3.9" + + - name: Run image + uses: abatilo/actions-poetry@v2 + with: + poetry-version: "1.6.1" + + - run: make install + - run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f94e5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +__pycache__ diff --git a/FrontLights.cpp b/FrontLights.cpp deleted file mode 100644 index d1294ca..0000000 --- a/FrontLights.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include -#include - -namespace SolarGators { -namespace DataModules { -namespace { - static constexpr uint32_t SIZE = 3; -} - -FrontLights::FrontLights(): - DataModule(SolarGators::DataModuleInfo::FRONT_LIGHTS_ID, 0, SIZE), - - throttle(0) - -{ } - -FrontLights::~FrontLights() -{ } - - -uint_16_t FrontLights::GetthrottleVal() const -{ - return throttle; -} - - -void FrontLights::ToByteArray(uint8_t* buff) const -{ - - - buff[0] = static_cast(throttle << 0); - - buff[1] = static_cast(throttle << 1); - - -} -void FrontLights::FromByteArray(uint8_t* buff) -{ - - throttle_ = static_cast(buff[1]) << 8 | buff[0]; - -} - -#ifdef IS_TELEMETRY -void FrontLights::PostTelemetry(PythonScripts* scripts) { - -} -#endif - -} /* namespace DataModules */ -} /* namespace SolarGators */ \ No newline at end of file diff --git a/Makefile b/Makefile index bf8778f..b03eccd 100644 --- a/Makefile +++ b/Makefile @@ -2,3 +2,7 @@ .PHONY: install install: poetry install + +.PHONY: test +test: + poetry run pytest diff --git a/README.md b/README.md new file mode 100644 index 0000000..59f126d --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Telemetry Parsing Codegen + +To eliminate repetitive code, this module takes in a yaml file with the schema of the CAN messages, and generates the cpp modules needed to parse the CAN messages. diff --git a/config.yml b/config.yml deleted file mode 100644 index 4292664..0000000 --- a/config.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Important things: -# - need to be able to support any type -# - need to be able to handle booleans -# - - -can_messages: - - id: 0x11 - name: "FrontLights" - schema: - # will parse the next two bytes - - type: "uint_16_t" - name: "throttle" - default: 0 - - # boolean map - - type: "boolean_map" - bits: - 0: - name: "brake" - default: false - - name: "SomeOtherModule" - dbc: "some_message.bdc" diff --git a/datamodule/__init__.py b/datamodule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/datamodule/cpp/DataModuleInfo.hpp b/datamodule/cpp/DataModuleInfo.hpp index e69de29..357772f 100644 --- a/datamodule/cpp/DataModuleInfo.hpp +++ b/datamodule/cpp/DataModuleInfo.hpp @@ -0,0 +1,21 @@ +/* + * DataModuleInfo.hpp + * + * THIS IS AUTO GENERATED. DO NOT EDIT + */ + +#ifndef SOLARGATORSBSP_DATAMODULES_INC_DATAMODULEINFO_HPP_ +#define SOLARGATORSBSP_DATAMODULES_INC_DATAMODULEINFO_HPP_ + + +namespace SolarGators::DataModuleInfo +{ +// ---- Addresses ---- // + +{% for message in canMessages %} +static constexpr uint32_t {{ message["name"] }}_ID = {{ message["id"] }}; +{% endfor %} +} + + +#endif /* SOLARGATORSBSP_DATAMODULES_INC_DATAMODULEINFO_HPP_ */ diff --git a/datamodule/cpp/module.cpp b/datamodule/cpp/module.cpp index 2509dfa..658ea67 100644 --- a/datamodule/cpp/module.cpp +++ b/datamodule/cpp/module.cpp @@ -1,5 +1,12 @@ -#include <{{ moduleName }}.hpp> -#include +/* + * {{ moduleName }}.cpp + * + * THIS IS AUTO GENERATED, DO NOT EDIT. + */ + +#include "{{ moduleName }}.hpp" +#include "DataModuleInfo.hpp" +#include namespace SolarGators { namespace DataModules { @@ -8,9 +15,12 @@ namespace { } {{ moduleName }}::{{ moduleName }}(): - DataModule(SolarGators::DataModuleInfo::FRONT_LIGHTS_ID, 0, SIZE), + DataModule(SolarGators::DataModuleInfo::{{ moduleName }}_ID, 0, SIZE), {% for attribute in attributes %} {{ attribute["name"] }}({{ attribute["default"] }}) + {% if not loop.last %} + , + {% endif %} {% endfor %} { } @@ -26,17 +36,19 @@ namespace { void {{ moduleName }}::ToByteArray(uint8_t* buff) const { + size_t index = 0; {% for attribute in attributes %} - {%- for byte in range(attribute["bytes"]) %} - buff[{{ byte }}] = static_cast({{ attribute["name"] }} >> {{ byte }}); - {% endfor %} + memcpy(buff + index, &{{ attribute["name"] }}, sizeof({{ attribute["name"] }})); + index += sizeof({{ attribute["name"] }}); {% endfor %} } + void {{ moduleName }}::FromByteArray(uint8_t* buff) { + size_t index = 0; {% for attribute in attributes %} - // TODO: This one is much harder and prone to error - throttle_ = static_cast(buff[1]) << 8 | buff[0]; + memcpy(&{{ attribute["name"] }}, buff + index,sizeof({{ attribute["name"] }})); + index += sizeof({{ attribute["name"] }}); {% endfor %} } diff --git a/datamodule/cpp/module.h b/datamodule/cpp/module.h index 623273f..06ab193 100644 --- a/datamodule/cpp/module.h +++ b/datamodule/cpp/module.h @@ -1,27 +1,26 @@ /* * FrontLights.hpp * - * Created on: Jan 17, 2022 - * Author: John Carr + * THIS IS AUTO GENERATED, DO NOT EDIT */ #ifndef SOLARGATORSBSP_STM_DATAMODULES_INC_FRONTLIGHTS_HPP_ #define SOLARGATORSBSP_STM_DATAMODULES_INC_FRONTLIGHTS_HPP_ -#include +#include "DataModule.hpp" #define BUFF_SIZE 50 namespace SolarGators { namespace DataModules { -class FrontLights: public DataModule { +class {{ moduleName }}: public DataModule { public: - FrontLights(); - ~FrontLights(); - uint16_t GetThrottleVal() const; - bool GetBreaksVal() const; - uint8_t buffCtr; - uint16_t breaksBuffer[BUFF_SIZE]; + {{ moduleName }}(); + ~{{ moduleName }}(); + {% for attribute in attributes %} + {{ attribute["type"] }} Get{{ attribute["name"] }}Val() const; + {% endfor %} + // CAN Functions void ToByteArray(uint8_t* buff) const; void FromByteArray(uint8_t* buff); @@ -30,10 +29,9 @@ class FrontLights: public DataModule { #endif protected: - uint16_t throttle_; - bool breaks_; - - // TODO: Accelerometer values + {% for attribute in attributes %} + {{ attribute["type"] }} {{ attribute["name"] }}; + {% endfor %} }; } /* namespace DataModules */ diff --git a/datamodule/generate.py b/datamodule/generate.py index 2e33046..26bad5b 100644 --- a/datamodule/generate.py +++ b/datamodule/generate.py @@ -1,24 +1,24 @@ -import os -import sys +from pathlib import Path import yaml -from jinja2 import Environment, FileSystemLoader, select_autoescape +from jinja2 import Environment, FileSystemLoader -def main(): - config_file = sys.argv[1] - +def main(config_file: str): with open(config_file, "r") as f: config = yaml.safe_load(f) - env = Environment(loader=FileSystemLoader(os.path.dirname(os.path.abspath(__file__)))) + env = Environment(loader=FileSystemLoader(Path(__file__).parent)) cpp_template = env.get_template("cpp/module.cpp") + hpp_template = env.get_template("cpp/module.h") + info_template = env.get_template("cpp/DataModuleInfo.hpp") for can_message in config["can_messages"]: + # ensure the fist letter is caps for attribute in can_message["schema"]: - attribute["bytes"] = 2 + attribute["name"] = attribute["name"][0].upper() + attribute["name"][1:] with open(f"{can_message['name']}.cpp", "w") as f: file = cpp_template.render( @@ -27,6 +27,19 @@ def main(): ) f.write(file) + with open(f"{can_message['name']}.hpp", "w") as f: + file = hpp_template.render( + moduleName=can_message["name"], + attributes=can_message["schema"], + ) + f.write(file) + + with open(f"DataModuleInfo.hpp", "w") as f: + file = info_template.render( + canMessages=config["can_messages"], + ) + f.write(file) + if __name__ == "__main__": - main() + main(sys.argv[1]) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/goldens/16bit/config.yml b/tests/goldens/16bit/config.yml new file mode 100644 index 0000000..e49883d --- /dev/null +++ b/tests/goldens/16bit/config.yml @@ -0,0 +1,11 @@ +can_messages: + - id: 0x11 + name: "RX0" + schema: + - type: "uint16_t" + name: "first" + default: 0 + + - type: "uint16_t" + name: "second" + default: 0 diff --git a/tests/goldens/16bit/test.cpp b/tests/goldens/16bit/test.cpp new file mode 100644 index 0000000..9aa799e --- /dev/null +++ b/tests/goldens/16bit/test.cpp @@ -0,0 +1,34 @@ +#include +#include +#include +#include +#include "RX0.hpp" + +int main() { + uint8_t data[4] = {0x37, 0x44, 0x20, 0x40}; + SolarGators::DataModules::RX0 RX0; + + RX0.FromByteArray(data); + + std::cout << "input first: " << RX0.GetFirstVal() << std::endl; + std::cout << "input second: " << RX0.GetSecondVal() << std::endl; + if (RX0.GetFirstVal() != 0x4437) { + std::cout << "first is wrong!" << std::endl; + return -1; + } + + if (RX0.GetSecondVal() != 0x4020) { + std::cout << "second is wrong!" << std::endl; + return -1; + } + + RX0.ToByteArray(data); + if (data[0] != 0x37 || data[1] != 0x44) { + std::cout << "first to byte array is wrong!" << std::endl; + return -1; + } + if (data[2] != 0x20 || data[3] != 0x40) { + std::cout << "second to byte array is wrong!" << std::endl; + return -1; + } +} diff --git a/tests/goldens/8bit/config.yml b/tests/goldens/8bit/config.yml new file mode 100644 index 0000000..d06793f --- /dev/null +++ b/tests/goldens/8bit/config.yml @@ -0,0 +1,11 @@ +can_messages: + - id: 0x11 + name: "RX0" + schema: + - type: "uint8_t" + name: "first" + default: 0 + + - type: "uint8_t" + name: "second" + default: 0 diff --git a/tests/goldens/8bit/test.cpp b/tests/goldens/8bit/test.cpp new file mode 100644 index 0000000..d854779 --- /dev/null +++ b/tests/goldens/8bit/test.cpp @@ -0,0 +1,34 @@ +#include +#include +#include +#include +#include "RX0.hpp" + +int main() { + uint8_t data[2] = {0x37, 0x44}; + SolarGators::DataModules::RX0 RX0; + + RX0.FromByteArray(data); + + std::cout << "input first: " << RX0.GetFirstVal() << std::endl; + std::cout << "input second: " << RX0.GetSecondVal() << std::endl; + if (RX0.GetFirstVal() != data[0]) { + std::cout << "first is wrong!" << std::endl; + return -1; + } + + if (RX0.GetSecondVal() != data[1]) { + std::cout << "second is wrong!" << std::endl; + return -1; + } + + RX0.ToByteArray(data); + if (data[0] != 0x37) { + std::cout << "first to byte array is wrong!" << std::endl; + return -1; + } + if (data[1] != 0x44) { + std::cout << "second to byte array is wrong!" << std::endl; + return -1; + } +} diff --git a/tests/goldens/floats/config.yml b/tests/goldens/floats/config.yml new file mode 100644 index 0000000..d495f05 --- /dev/null +++ b/tests/goldens/floats/config.yml @@ -0,0 +1,11 @@ +can_messages: + - id: 0x11 + name: "RX0" + schema: + - type: "float" + name: "first" + default: 0 + + - type: "float" + name: "second" + default: 0 diff --git a/tests/goldens/floats/test.cpp b/tests/goldens/floats/test.cpp new file mode 100644 index 0000000..289a4a6 --- /dev/null +++ b/tests/goldens/floats/test.cpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include +#include "RX0.hpp" + +int main() { + uint8_t data[8]; + float first = 1.1; + float second = 2.2; + memcpy(&data[0], &first, sizeof(first)); + memcpy(&data[4], &second, sizeof(second)); + + SolarGators::DataModules::RX0 RX0; + + RX0.FromByteArray(data); + + std::cout << "input first: " << RX0.GetFirstVal() << std::endl; + std::cout << "input second: " << RX0.GetSecondVal() << std::endl; + if (RX0.GetFirstVal() != first) { + std::cout << "first is wrong!" << std::endl; + return -1; + } + + if (RX0.GetSecondVal() != second) { + std::cout << "second is wrong!" << std::endl; + return -1; + } +} diff --git a/tests/headers/DataModule.hpp b/tests/headers/DataModule.hpp new file mode 100644 index 0000000..eda323d --- /dev/null +++ b/tests/headers/DataModule.hpp @@ -0,0 +1,49 @@ +/* + * DataModule.h + * + * Created on: Oct 29, 2021 + * Author: John Carr + */ + +#ifndef SOLARGATORSBSP_DATAMODULES_INC_DATAMODULE_HPP_ +#define SOLARGATORSBSP_DATAMODULES_INC_DATAMODULE_HPP_ + +#include + +namespace SolarGators { +namespace DataModules { + +class DataModule { +public: + DataModule(uint32_t can_id, uint16_t telem_id, uint32_t size, uint16_t instance_id = 0, bool is_ext_id = false, bool is_rtr = false): + can_id_(can_id), telem_id_(telem_id), size_(size), instance_id_(instance_id), is_ext_id_(is_ext_id), is_rtr_(is_rtr) + { + #ifndef IS_TELEMETRY + // mutex_id_ = osMutexNew(&mutex_attributes_); + #endif + }; + virtual ~DataModule() {}; + // All data modules must define this so that we can parse the can messages + virtual void ToByteArray(uint8_t* buff) const = 0; + virtual void FromByteArray(uint8_t* buff) = 0; + #ifdef IS_TELEMETRY + virtual void PostTelemetry(PythonScripts* scripts) = 0; + #endif + // The can bus ID for the data module + const uint32_t can_id_; + // ID for transmitting the data module to the pit + const uint16_t telem_id_; + // Amount of data in bytes + const uint32_t size_; + // Instance ID + const uint16_t instance_id_; + // If the can bus ID is extended + const bool is_ext_id_; + // If the can message is RTR + const bool is_rtr_; +}; + +} /* namespace DataModules */ +} /* namespace SolarGators */ + +#endif /* SOLARGATORSBSP_DATAMODULES_INC_DATAMODULE_HPP_ */ diff --git a/tests/test_goldens.py b/tests/test_goldens.py new file mode 100644 index 0000000..86041bf --- /dev/null +++ b/tests/test_goldens.py @@ -0,0 +1,34 @@ +from pathlib import Path +import shutil, os, pytest +from datamodule.generate import main + +@pytest.fixture +def tempwd(tmpdir: Path): + org_dir = os.getcwd() + os.chdir(tmpdir) + yield tmpdir + os.chdir(org_dir) + + +@pytest.mark.parametrize( + "test_dir", + [ + "8bit", + "16bit", + "floats" + ] +) +def test_golden_template(tempwd: Path, test_dir: str): + + # copy into test directory + shutil.copytree(Path(__file__).parent / f"goldens/{test_dir}", tempwd, dirs_exist_ok=True) + shutil.copytree(Path(__file__).parent / "headers", tempwd, dirs_exist_ok=True) + + # generate from config + main("config.yml") + + # compile tests + assert os.system('g++ test.cpp RX0.cpp -std=c++11 -o test') == 0 + + # run cpp tests + assert os.system('./test') == 0