From 45bfc724fceb8cd47e7f7fce34ba800d068c4022 Mon Sep 17 00:00:00 2001 From: AlexanderHD27 Date: Mon, 14 Apr 2025 14:43:03 +0200 Subject: [PATCH] Init Commit: Moved bproto to seperate repo --- .coveragerc | 14 + .flake8 | 15 + .gitignore | 12 + .gitmodules | 3 + .vscode/launch.json | 77 + .vscode/settings.json | 33 + .vscode/tasks.json | 155 ++ Dockerfile | 17 + LICENSE.md | 7 + README.md | 122 ++ TODOs.md | 24 + bprotoV1.g4 | 39 + coverage/cov_c.xml | 3 + coverage/cov_python.xml | 1554 +++++++++++++++++ docs/bprotoDocumentationGraphics.drawio | 57 + docs/compatibility.md | 75 + .../bprotoDocumentationGraphics.drawio.png | Bin 0 -> 35396 bytes docs/serialization.md | 66 + docs/syntax.md | 221 +++ pytest.ini | 4 + requirements.txt | 11 + run_docker_compiler.py | 50 + scripts/docker_and_push.bash | 3 + src/backend/__init__.py | 15 + src/backend/fsOutput.py | 237 +++ src/backend/rendering/cBackend.py | 270 +++ src/backend/rendering/pythonBackend.py | 244 +++ src/backend/rendering/txtBackend.py | 71 + src/compiler.py | 54 + src/errors.py | 113 ++ src/gen/bprotoV1.interp | 62 + src/gen/bprotoV1.tokens | 29 + src/gen/bprotoV1Lexer.interp | 68 + src/gen/bprotoV1Lexer.py | 106 ++ src/gen/bprotoV1Lexer.tokens | 29 + src/gen/bprotoV1Listener.py | 165 ++ src/gen/bprotoV1Parser.py | 1266 ++++++++++++++ src/gen/bprotoV1Visitor.py | 98 ++ src/main.py | 62 + src/nameHandling/base.py | 169 ++ src/nameHandling/brand_applier.py | 53 + src/nameHandling/resolver.py | 95 + src/nameHandling/style/cNameStyle.py | 75 + src/nameHandling/style/pythonNameStyle.py | 57 + src/parser/ast_visitor.py | 157 ++ src/parser/parser.py | 120 ++ src/protocol_components/__init__.py | 91 + src/protocol_components/bitfields.py | 167 ++ src/protocol_components/crc.py | 22 + src/protocol_components/dtypes.py | 153 ++ src/protocol_components/enumeration.py | 232 +++ src/protocol_components/field.py | 337 ++++ src/protocol_components/message.py | 144 ++ src/protocol_components/protocol.py | 57 + src/protocol_components/protocolFactory.py | 107 ++ template/c/template/bproto.cmake.jinja2 | 14 + .../include/bitfield/bitfield.h.jinja2 | 18 + template/c/template/include/crc.h.jinja2 | 18 + template/c/template/include/enums.h.jinja2 | 26 + .../template/include/message/message.h.jinja2 | 27 + template/c/template/src/bitfield.c.jinja2 | 6 + .../template/src/bitfield/fromBytes.c.jinja2 | 10 + .../c/template/src/bitfield/toBytes.c.jinja2 | 12 + template/c/template/src/crc.c.jinja2 | 48 + .../c/template/src/message/fromBytes.c.jinja2 | 70 + .../c/template/src/message/mapping.c.jinja2 | 14 + .../c/template/src/message/toBytes.c.jinja2 | 34 + template/python/static/bproto_base.py | 67 + template/python/static/bproto_error.py | 19 + .../bproto_protocol_bitfield.py.jinja2 | 38 + .../bproto_protocol_packets.py.jinja2 | 19 + .../bproto_protocol_packets_ids.py.jinja2 | 15 + template/python/template/enum.py.jinja2 | 8 + template/python/template/message/class.jinja2 | 20 + .../python/template/message/fromBytes.jinja2 | 21 + .../template/message/parse_generic.jinja2 | 31 + .../python/template/message/toBytes.jinja2 | 8 + template/txt/protocolSummary.txt.jinja2 | 26 + test/compiler/ast/test_ast_message.py | 38 + test/compiler/ast/test_ast_tag_line.py | 23 + test/compiler/backend/test_comiler_c.py | 26 + test/compiler/backend/test_compiler_python.py | 59 + test/compiler/backend/test_compiler_txt.py | 32 + test/compiler/backend/test_fsOutput.py | 168 ++ .../name_conversion/test_name_bproto_style.py | 49 + .../test_name_brand_applier.py | 40 + .../name_conversion/test_name_c_style.py | 73 + .../name_conversion/test_name_python_style.py | 59 + test/compiler/test_bitfields.py | 80 + test/compiler/test_deep_copy.py | 73 + test/compiler/test_emums.py | 228 +++ test/compiler/test_field.py | 98 ++ test/compiler/test_message.py | 71 + test/compiler/test_protocol.py | 181 ++ test/data/static_content1.txt | 1 + test/data/static_folder/static_content2.txt | 1 + test/data/templates/template.jinja2 | 1 + test/output/c/CMakeLists.txt | 48 + test/output/c/src/Unity | 1 + test/output/c/src/main.c | 17 + test/output/c/src/suit.h | 9 + test/output/c/src/test_messages.c | 224 +++ test/output/c/src/test_python_integration.c | 84 + test/output/test_c_integration.py | 162 ++ test/output/test_crc.py | 63 + test/output/test_encdec.py | 197 +++ .../reference/python/HCP_protocol_bitfield.py | 82 + test/reference/python/HCP_protocol_enum.py | 19 + .../python/HCP_protocol_message_ids.py | 12 + test/reference/python/HCP_protocol_packets.py | 134 ++ test/reference/txt/protocolSummary.txt | 49 + test/runner.py | 4 + test/test_protocol.bproto | 39 + vscode/bproto/.vscode/launch.json | 17 + vscode/bproto/.vscodeignore | 4 + vscode/bproto/CHANGELOG.md | 9 + vscode/bproto/LICENSE | 7 + vscode/bproto/README.md | 3 + vscode/bproto/bproto-0.0.1.vsix | Bin 0 -> 4779 bytes vscode/bproto/bproto-0.0.2.vsix | Bin 0 -> 4778 bytes vscode/bproto/language-configuration.json | 23 + vscode/bproto/package.json | 26 + vscode/bproto/syntaxes/bproto.tmLanguage.json | 166 ++ vscode/bproto/test_protocol.bproto | 37 + vscode/bproto/vsc-extension-quickstart.md | 29 + 125 files changed, 10822 insertions(+) create mode 100644 .coveragerc create mode 100644 .flake8 create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 Dockerfile create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 TODOs.md create mode 100644 bprotoV1.g4 create mode 100644 coverage/cov_c.xml create mode 100644 coverage/cov_python.xml create mode 100644 docs/bprotoDocumentationGraphics.drawio create mode 100644 docs/compatibility.md create mode 100644 docs/img/bprotoDocumentationGraphics.drawio.png create mode 100644 docs/serialization.md create mode 100644 docs/syntax.md create mode 100644 pytest.ini create mode 100644 requirements.txt create mode 100755 run_docker_compiler.py create mode 100755 scripts/docker_and_push.bash create mode 100644 src/backend/__init__.py create mode 100644 src/backend/fsOutput.py create mode 100644 src/backend/rendering/cBackend.py create mode 100644 src/backend/rendering/pythonBackend.py create mode 100644 src/backend/rendering/txtBackend.py create mode 100644 src/compiler.py create mode 100644 src/errors.py create mode 100644 src/gen/bprotoV1.interp create mode 100644 src/gen/bprotoV1.tokens create mode 100644 src/gen/bprotoV1Lexer.interp create mode 100644 src/gen/bprotoV1Lexer.py create mode 100644 src/gen/bprotoV1Lexer.tokens create mode 100644 src/gen/bprotoV1Listener.py create mode 100644 src/gen/bprotoV1Parser.py create mode 100644 src/gen/bprotoV1Visitor.py create mode 100644 src/main.py create mode 100644 src/nameHandling/base.py create mode 100644 src/nameHandling/brand_applier.py create mode 100644 src/nameHandling/resolver.py create mode 100644 src/nameHandling/style/cNameStyle.py create mode 100644 src/nameHandling/style/pythonNameStyle.py create mode 100644 src/parser/ast_visitor.py create mode 100644 src/parser/parser.py create mode 100644 src/protocol_components/__init__.py create mode 100644 src/protocol_components/bitfields.py create mode 100644 src/protocol_components/crc.py create mode 100644 src/protocol_components/dtypes.py create mode 100644 src/protocol_components/enumeration.py create mode 100644 src/protocol_components/field.py create mode 100644 src/protocol_components/message.py create mode 100644 src/protocol_components/protocol.py create mode 100644 src/protocol_components/protocolFactory.py create mode 100644 template/c/template/bproto.cmake.jinja2 create mode 100644 template/c/template/include/bitfield/bitfield.h.jinja2 create mode 100644 template/c/template/include/crc.h.jinja2 create mode 100644 template/c/template/include/enums.h.jinja2 create mode 100644 template/c/template/include/message/message.h.jinja2 create mode 100644 template/c/template/src/bitfield.c.jinja2 create mode 100644 template/c/template/src/bitfield/fromBytes.c.jinja2 create mode 100644 template/c/template/src/bitfield/toBytes.c.jinja2 create mode 100644 template/c/template/src/crc.c.jinja2 create mode 100644 template/c/template/src/message/fromBytes.c.jinja2 create mode 100644 template/c/template/src/message/mapping.c.jinja2 create mode 100644 template/c/template/src/message/toBytes.c.jinja2 create mode 100644 template/python/static/bproto_base.py create mode 100644 template/python/static/bproto_error.py create mode 100644 template/python/template/bproto_protocol_bitfield.py.jinja2 create mode 100644 template/python/template/bproto_protocol_packets.py.jinja2 create mode 100644 template/python/template/bproto_protocol_packets_ids.py.jinja2 create mode 100644 template/python/template/enum.py.jinja2 create mode 100644 template/python/template/message/class.jinja2 create mode 100644 template/python/template/message/fromBytes.jinja2 create mode 100644 template/python/template/message/parse_generic.jinja2 create mode 100644 template/python/template/message/toBytes.jinja2 create mode 100644 template/txt/protocolSummary.txt.jinja2 create mode 100644 test/compiler/ast/test_ast_message.py create mode 100644 test/compiler/ast/test_ast_tag_line.py create mode 100644 test/compiler/backend/test_comiler_c.py create mode 100644 test/compiler/backend/test_compiler_python.py create mode 100644 test/compiler/backend/test_compiler_txt.py create mode 100644 test/compiler/backend/test_fsOutput.py create mode 100644 test/compiler/name_conversion/test_name_bproto_style.py create mode 100644 test/compiler/name_conversion/test_name_brand_applier.py create mode 100644 test/compiler/name_conversion/test_name_c_style.py create mode 100644 test/compiler/name_conversion/test_name_python_style.py create mode 100644 test/compiler/test_bitfields.py create mode 100644 test/compiler/test_deep_copy.py create mode 100644 test/compiler/test_emums.py create mode 100644 test/compiler/test_field.py create mode 100644 test/compiler/test_message.py create mode 100644 test/compiler/test_protocol.py create mode 100644 test/data/static_content1.txt create mode 100644 test/data/static_folder/static_content2.txt create mode 100644 test/data/templates/template.jinja2 create mode 100644 test/output/c/CMakeLists.txt create mode 160000 test/output/c/src/Unity create mode 100644 test/output/c/src/main.c create mode 100644 test/output/c/src/suit.h create mode 100644 test/output/c/src/test_messages.c create mode 100644 test/output/c/src/test_python_integration.c create mode 100644 test/output/test_c_integration.py create mode 100644 test/output/test_crc.py create mode 100644 test/output/test_encdec.py create mode 100644 test/reference/python/HCP_protocol_bitfield.py create mode 100644 test/reference/python/HCP_protocol_enum.py create mode 100644 test/reference/python/HCP_protocol_message_ids.py create mode 100644 test/reference/python/HCP_protocol_packets.py create mode 100644 test/reference/txt/protocolSummary.txt create mode 100644 test/runner.py create mode 100644 test/test_protocol.bproto create mode 100644 vscode/bproto/.vscode/launch.json create mode 100644 vscode/bproto/.vscodeignore create mode 100644 vscode/bproto/CHANGELOG.md create mode 100644 vscode/bproto/LICENSE create mode 100644 vscode/bproto/README.md create mode 100644 vscode/bproto/bproto-0.0.1.vsix create mode 100644 vscode/bproto/bproto-0.0.2.vsix create mode 100644 vscode/bproto/language-configuration.json create mode 100644 vscode/bproto/package.json create mode 100644 vscode/bproto/syntaxes/bproto.tmLanguage.json create mode 100644 vscode/bproto/test_protocol.bproto create mode 100644 vscode/bproto/vsc-extension-quickstart.md diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..c0ad22f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,14 @@ +[report] +exclude_also = + def __repr__ +precision = 2 +sort = Name + +[xml] +output = coverage/cov_python.xml + +[run] +branch = True +include = test/backend_output/python/*, + src/*, +omit = src/gen/* \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..286b671 --- /dev/null +++ b/.flake8 @@ -0,0 +1,15 @@ +[flake8] +exclude = + src/gen/**/*.py, + __pycache__, + .git +filename = + src/**/*.py, + test/**/*.py + + + +ignore = + W293, + E501, + E402 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b88b2b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.antlr +.venv +.pytest_cache +.coverage +.$* + +**/__pycache__ +test_out/* +**/.cmake +**/build + +test/backend_output/* \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4d4ba9d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test/output/c/src/Unity"] + path = test/output/c/src/Unity + url = https://github.com/ThrowTheSwitch/Unity diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e7df09e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,77 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: bproto compiler Python", + "type": "debugpy", + "program": "src/main.py", + "console": "integratedTerminal", + "args": [ + "-i", "test/test_protocol.bproto", + "-o", "test/python/bproto/", + "-t", "python" + ] + }, + { + "name": "Pytest: Python Backend Tests", + "type": "debugpy", + "request": "launch", + "program": "test/runner.py", + "console": "integratedTerminal", + }, + + { + "name": "Tests C Debug (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/test_runner", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/test/backend_output/c", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ], + "preLaunchTask": "CMake: Build C Tests" + }, + { + "name": "Tests C Python Integration Debug (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/test_integration_python", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/test/backend_output/c", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ], + "preLaunchTask": "CMake: Build C Tests" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5b0c2f2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,33 @@ +{ + "files.associations": { + "cmath": "cpp", + "type_traits": "cpp", + "__nullptr": "cpp", + "target": "cpp", + "bproto_encdec.h": "c", + "functional": "c", + "__locale": "c", + "bproto_package_ids.h": "c", + "bproto_package_structs.h": "c", + "random": "cpp", + "algorithm": "cpp", + "suit.h": "c", + "stdint.h": "c", + "hcp_message.h": "c" + }, + "cmake.sourceDirectory": "/home/alexander/Projects/tumRobotic25/protocolGenerator/test/output/c", + "coverage-gutters.coverageFileNames": [ + "coverage/cov_c.xml", + "coverage/cov_python.xml", + ], + "coverage-gutters.showGutterCoverage": false, + "coverage-gutters.showLineCoverage": true, + "flake8.enabled": true, + "flake8.args": [ + "--config=.flake8", + "--count" + ], + "flake8.ignorePatterns": [ + "src/gen/*.py" + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..34b1d96 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,155 @@ +{ + "version": "2.0.0", + "tasks": [ + {"label": "Antlr: Generate Parser", + "type": "shell", + "command": "antlr4", + "args": [ + "-Dlanguage=Python3", + "-visitor", + "-o", + "src/gen", + "bprotoV1.g4" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] + }, + {"label": "Bproto compiler C", + "type": "shell", + "command": "${command:python.interpreterPath}", + "args": [ + "src/main.py", + "-i", "test/test_protocol.bproto", + "-o", "test/backend_output", + "-t", "c" + ], + "problemMatcher": [] + }, + {"label": "CMake: Configure C Test", + "type": "cmake", + "options": { + "cwd": "${workspaceFolder}/test/output/c" + }, + "command": "configure", + "args": [ + "-DCMAKE_BUILD_TYPE=Debug" + ], + "problemMatcher": [] + }, + {"label": "CMake: Build C Tests", + "type": "cmake", + "options": { + "cwd": "${workspaceFolder}/test/output/c" + }, + "command": "build", + "args": [ + "all" + ], + "problemMatcher": ["$gcc"], + "dependsOn": ["Bproto compiler C"], + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "dedicated", + "showReuseMessage": true, + "clear": true + } + }, + {"label": "Run C Tests", + "type": "shell", + "command": "build/test_runner", + "problemMatcher": ["$gcc"], + "dependsOn": ["CMake: Build C Tests"], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "dedicated", + "showReuseMessage": true, + "clear": true + } + }, + {"label": "gcovr: Generate C Coverage", + "type": "shell", + "command": "gcovr", + "args": [ + "-p", "--txt", + "--xml", "coverage/cov_c.xml", + "--root", ".", + "--object-directory", "build/CMakeFiles/test_runner.dir/src/" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "dependsOn": ["Run C Tests"], + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "dedicated", + "showReuseMessage": true, + "clear": true + } + }, + + {"label": "Run Compiler Python", + "type": "shell", + "command": "${command:python.interpreterPath}", + "args": [ + "src/main.py", + "-i", "test/test_protocol.bproto", + "-o", "test/backend_output", + "-t", "python" + ], + "problemMatcher": [] + }, + {"label": "pytest coverage", + "type": "shell", + "command": "${command:python.interpreterPath}", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "TEST_COVERAGE": "1" // This cause integration tests to be ignored, as they push up the coverage + } + }, + "args": [ + "-m", + "pytest", + "--cov", + "--cov-report=term", + "--cov-report=xml", + ], + "problemMatcher": [], + "dependsOn": ["Run Compiler Python"] + }, + {"label": "pytest test", + "type": "shell", + "command": "${command:python.interpreterPath}", + "options": { + "cwd": "${workspaceFolder}", + }, + "args": [ + "-m", + "pytest", + "--cov", + "--cov-report=term", + "--cov-report=xml", + ], + "problemMatcher": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "dedicated", + "showReuseMessage": true, + "clear": true + }, + "dependsOn": [ "Run Compiler Python" ] + }, + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..75170ce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:alpine3.21 + +WORKDIR /compiler +ADD src /compiler/src +ADD template /compiler/template +ADD requirements.txt . +RUN pip install -r requirements.txt + +RUN addgroup --g 1000 groupcontainer +RUN adduser -u 1000 -G groupcontainer -h /home/containeruser -D containeruser + +USER containeruser + +WORKDIR /compiler/data +WORKDIR /compiler + +ENTRYPOINT [ "python3" ] \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..9e71eef --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2025 AlexanderHD27 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..af7a051 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# bproto +_binarized protocol_ + +A protocol description language for generating lightweight, binary formats optimized for embedded systems and constrained environments. + +Bproto is a transpiler from the bproto format to a target language. Unlike protobuf and JSON, bproto focuses on creating highly efficient binary formats suitable for constrained environments. + +bproto is designed to be statically typed and sized. No memory allocation is needed for serialization and deserialization, making it suitable for embedded systems and real-time applications. + +**bproto supports:** +- Numerical, Floating-point, and Boolean datatypes +- Fixed-size arrays and strings +- Enums and Bitfields + +**bproto does not support:** +- Variable-length arrays or strings +- Nested messages +- Optional fields + +**Current supported Targets: C, Python** + +Planned support: Markdown Documentation, TypeScript/Deno + +## Getting Start + +For Beginners have a look at the [Getting Started Guide](/docs/getting_started.md) + +Currently the easiest way to run the bproto-compiler is via `run_docker_compiler.py`. +For that you need docker ([Install Docker](https://docs.docker.com/engine/install/)) and python ([Install Python](https://www.python.org/downloads/)). + +Running the compiler: +```bash +./run_docker_compiler.py --input protocol_description_file.bproto --output output_folder/ --target txt --target python --target c +``` + +This will compile the `protocol_description_file.bproto` to `txt`, `python` and `c`. The output is placed in `output_folder/` + +This use the `alexanderhd27/bproto-compiler:latest` docker image + +### Running it nativ + +You need python3.12. +1. Install the python requirements with `pip install -r requirements.txt` +2. Running the compiler with +``` +--input protocol_description_file.bproto --output output_folder/ --target txt --target python --target c +``` + +IMPORTANTED: You need to run the compiler from `protocolGenerator` directory, such that the compiler has access to template folder + +## Core concept + +How does bproto work? There are three main components: + +Diagram showing the flow from the protocol definition via the bproto-compiler to the protocol implementation (C and Python) + +
+ +- **Protocol Description** – A structured definition of the binary format, specifying message types, field layouts, and encoding rules. +- **bproto-Compiler** – Translates the protocol description into optimized source code, generating efficient serializers and deserializers. +- **Protocol Implementation (aka the target)** – The generated code and supporting files that implement the described protocol, ready for integration into (embedded) applications. + +
+ +## Protocol Description + +A protocol is described in a .bproto file. + +Example Protocol for an imaginary aircraft: +```bproto +protocol myCustomAirplaneProtocol version 2 + +enum engine_status { + OFF = 0, + IDLE = 1, + RUNNING = 2, + FULL_THRUST = 3 +} + +bits engine_enables { + engine_left, engine_right +} + +// Message or Package sent from engine to cockpit engine display +message [1] EngineSensorData { + [0] engine_thrust_left : float32, + [1] engine_thrust_right : float32, + [2] fuel_tanks_fill : uint32[8], // Fill levels of 8 fuel tanks + [3] engine_fault_left : bool, + [4] engine_fault_right : bool +} + +// Message or Package sent from cockpit to the engine controller +message [2] EngineCtrlCommand { + [0] engine_status : enum engine_status, + [1] engine_enables : bits engine_enables, + [2] flaps : int8, + [3] rudder : float64, + [4] trim : int16 +} +``` +(this is just an example. bproto should NOT be used in an airplane or any system that can cause harm) + +More about bproto's syntax can be found in the [syntax documentation](/docs/syntax.md). + +### Core Description Elements: + +- **Protocol Tag Line** – First Line of every protocol description. Defines the protocol name and version. It establishes the protocol's identity and ensures compatibility across protocol implementations. +- **Message** – A data structure designed to be sent over the wire. It contains fields with specific types and orders, representing the data to be transmitted. +- **Enum** – A set of named integer values that represent discrete states or options. Enums enhance code clarity by using descriptive names instead of raw numeric values. +- **Bitfields** – A compact representation of multiple boolean flags within a single byte or group of bytes, optimizing space for binary communication. + +Bitfields and Enums can also be declared inline in messages. + +Syntax documentation details can be found here! + +## Tooling/Versions for testing +gcc: 11.4.0 +gcov: 11.4.0 +gcovr: 5.0 + + diff --git a/TODOs.md b/TODOs.md new file mode 100644 index 0000000..b208cc5 --- /dev/null +++ b/TODOs.md @@ -0,0 +1,24 @@ +## TODOs: +- [X] Create pytest unit test +- [X] Create combine unit test (python -> c -> python) +- [X] Package ID + Sizes +- [X] Generic Deocde (decode with Id) +- [X] Converage Reports + +- [X] Clean Up Code / Add consitanzie for Code +- [ ] Add _Proper_ docs (code, dev tooling and product) +- [ ] Dockerize for simplification + +- [ ] Markdown Backend +- [X] Hash + CRC checksum +- [X] Proper Docs +- [X] Vscode Plugin Syntax Highlighting + +- [X] Add __eq__ for bitfields + +- [X] Bitfield and +- [X] Handle different endiandiss on differnt architectures (currently only little) +- [ ] Current: using memcpy to handle diffrent endiandiss, is there a bette way? + +- [X] Introduce flake8 +- [X] Big Reafactor \ No newline at end of file diff --git a/bprotoV1.g4 b/bprotoV1.g4 new file mode 100644 index 0000000..d788981 --- /dev/null +++ b/bprotoV1.g4 @@ -0,0 +1,39 @@ + +grammar bprotoV1; + +protocol_defintion : protocol_header (message_def | enum_def | bit_field_def)+; + +protocol_header : 'protocol' IDENTIFIER 'version' INT ; + +// Protocol definitions +message_def : 'message' message_id IDENTIFIER '{' (field ',')* (field ','?)? '}' ; +message_id : '[' INT ']'; +enum_def : 'enum' IDENTIFIER enum_body; +bit_field_def : 'bits' IDENTIFIER bitfield_body; + +field : field_pos IDENTIFIER ':' dtype; +field_pos : '[' INT ']'; + +dtype: type_standard | type_enum | type_bitfield; + +type_standard : IDENTIFIER array_extension?; +type_enum : 'enum' (enum_body | IDENTIFIER); +type_bitfield : 'bits' (bitfield_body | IDENTIFIER); + +array_extension: '[' INT ']'; + +// enum def +enum_body : '{' (enum_field ',')* (enum_field ','?)? '}'; +enum_field: IDENTIFIER ('=' INT)? ; + +// bit field def +bitfield_body : '{' (bitfield_field ',')* (bitfield_field ','?)? '}'; +bitfield_field: IDENTIFIER (':' INT)?; + +// Lexer rules +INT : [0-9]+; +IDENTIFIER : [a-zA-Z0-9_]+; + +WS : [ \t\r\n]+ -> skip; +COMMENT : '//' ~[\r\n]* -> skip; +COMMENT_MULTILINE : '/*' .*? '*/' -> skip; \ No newline at end of file diff --git a/coverage/cov_c.xml b/coverage/cov_c.xml new file mode 100644 index 0000000..b10edab --- /dev/null +++ b/coverage/cov_c.xml @@ -0,0 +1,3 @@ + + +. \ No newline at end of file diff --git a/coverage/cov_python.xml b/coverage/cov_python.xml new file mode 100644 index 0000000..a71f5cd --- /dev/null +++ b/coverage/cov_python.xml @@ -0,0 +1,1554 @@ + + + + + + /home/alexander/Projects/binarized_protocol_generator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/bprotoDocumentationGraphics.drawio b/docs/bprotoDocumentationGraphics.drawio new file mode 100644 index 0000000..e4a77de --- /dev/null +++ b/docs/bprotoDocumentationGraphics.drawio @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 0000000..71e5fa8 --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,75 @@ +# Compatibility and Versioning + +This document describes how bproto handles compatibility and versioning of protocols and messages. + +When receiving a message, the receiver must know if the data is compatible with the hard-coded protocol implementation. +There are multiple mechanisms to ensure this. + +The worst-case scenario is that the receiver gets a message that is not compatible with the protocol implementation -> meaning something has changed in the protocol and the receiver is not aware of it. + +## Compatibility + +**A protocol/message is compatible if** +- ✅ protocol and message definitions are the same +- ✅ the order of declaration of fields or messages has changed (but not the ID) + +**A protocol/message is incompatible if** +- ❌ If the protocol version has changed +- ❌ If a message has been removed or added +- ❌ If the protocol, message, bitfield, enum, or field name has changed +- ❌ If Message or Field ID has changed +- ❌ the number, type, order, or array size of fields in a message has changed +- ❌ If the order of declaration of enums or bitfields has changed + +=> In general, the receiver must have the same protocol implementation build as the sender. + +## Naming Conventions +Use descriptive names that clearly indicate the purpose of the protocol. Avoid generic names. Also include a hint about the fact that this is for transmitting data like "protocol" or "datastream". + +## Versioning Strategies +The version number is used to differentiate between different iterations of the protocol. Different versions of the same protocol are incompatible. By changing the protocol version, the developer can indicate a major change in structure, design, or purpose of the protocol/messages.Evalia + +**Example:** +```bproto +protocol robotControlProtocol version 1 +``` + +More about bproto's syntax can be found in the [syntax documentation](/docs/syntax.md#protocol-tag-line). + +## Compatibility Validation Process in Detail + +This is the message validation process that should prevent incompatible messages from being processed, causing undefined behavior. + +**Step #1 - Message Size Check:** +The first byte of the message is the message ID. +The receiver can with the message ID derive the expected message size. (If the message ID is unknown, the size is set to 0) +The receiver can then check if the received message is of the expected size. +If the size is not as expected, the message is discarded. +-> This prevents messages with changed field/message sizes from being processed. + +**Step #2 - CRC-16 Check:** +The CRC-16 is calculated over the message data and compared to the last 2 bytes of the message. +If the CRC-16 is not correct, the message is discarded. + +The CRC-16 is initialized by first prepending a SHA-256 hash of a text form of the protocol definition to the message data. +This text form is a human-readable representation of the protocol definition but also represents the internal structure of the protocol used by the bproto-compiler to generate other protocol implementations. +If something changes in this internal structure, a potential incompatibility is introduced. + +More details about how the CRC-16 is calculated can be found in the [serialization documentation](/docs/serialization.md#crc-16-calculation). + +This text form can be generated by the bproto-compiler when choosing the backend "txt". +This will also display the CRC-16 initialization vector and SHA-256 hash of the protocol definition. + +There are two possible reasons for a failed CRC check: +1. The message has been corrupted during transmission. +2. The message data is fine but the protocol definition has changed, and therefore the SHA-256 hash is different. + +=> The CRC-16 protects against data corruption and protocol changes. + +**Step #3 - Message ID Check:** +The receiver checks if the message ID is known. +If the message ID is unknown, the message is discarded. + +After Step #2 the receiver should already have discarded the message if it is incompatible with the protocol implementation. Nevertheless, this step is an additional line of defense. + + diff --git a/docs/img/bprotoDocumentationGraphics.drawio.png b/docs/img/bprotoDocumentationGraphics.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..01a5d88df027692d0c4fee3851e79ef2eab7ba5c GIT binary patch literal 35396 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 A!vFvP literal 0 HcmV?d00001 diff --git a/docs/serialization.md b/docs/serialization.md new file mode 100644 index 0000000..d7fb924 --- /dev/null +++ b/docs/serialization.md @@ -0,0 +1,66 @@ +# Message Encoding + +This section describes how messages are encoded/serialized and decoded/deserialized. +bproto is binary data with a well-defined structure. + +## Message Structure + +Every bproto message looks like this: + +``` +[ 1 ] [ N ] [ 2 ] +[Message ID] [Field Data] [CRC-16] +``` + +- **Message ID:** The first byte of the message is the message ID. This is used to identify the message type for decoding. Every message is declared with a unique ID in the protocol definition. See [Message](/docs/syntax.md#message) +- **Field Data:** The field data is the serialized data of the message fields. See [Message Field Data Encoding](#message-field-data-encoding) +- **CRC-16:** The last 2 bytes of the message are the CRC-16 checksum of the message data. This is used to verify the integrity of the message. See [CRC-16 Check](/docs/compatibility.md#step-2---crc-16-check) and [CRC-16 Calculation](/docs/serialization.md#crc-16-calculation) + +### Message Field Data Encoding + +The order, length, and type of the fields are defined in the protocol definition. +This means that bproto message structure is fixed and can be decoded without any additional delimiters or markers. + +The fields are just concatenated together in the order of their field ID. +Field IDs are declared in the protocol definition and are declaration order independent (see [Syntax of Fields](/docs/syntax.md#fields)). + +All numeric data types are serialized in big-endian byte order. +Data types are serialized as follows: +- For floating point numbers, the IEEE 754 standard is used. +- Signed integers are serialized as two's complement. +- Unsigned integers are serialized as is. +- Char and string are serialized as ASCII characters. +- Bool is serialized as 0 for false and 1 for true. +- Enums are serialized as 8-bit unsigned integers. +- Bitfields are serialized as a single (or multiple) byte with each bit representing a boolean value. The order of the bits is from the least significant bit to the most significant bit and from the first byte to the last byte. + +Datatype sizes are fixed and are documented in the datatypes-table in the [Datatypes](/docs/syntax.md#datatypes) section. +compatiblity +For arrays, the elements are serialized in order, with the first element first, the second element second, and so on. + +### Example Message Encoding + +An example of a message and serialization with 3 fields: +``` +message [1] ExampleMessage { + [0] a : uint8, + [1] b : uint16, + [2] c : bool +} +``` +with the values a=42, b=12345, c=true would be serialized as: `012a390501` +``` +[Message ID] [Field a] [Field b] [Field c] [CRC16] +[ 01 ] [ 2a ] [ 3905 ] [ 01 ] [31 06] +``` + +### CRC-16 Calculation + +The CRC-16 has the purpose of verifying the integrity of the message and preventing incompatible messages/protocol versions from being processed. + +When performing the CRC-16 calculation, the CRC-16 is initialized by first prepending a SHA-256 hash of a text form of the protocol definition to the message data. +This text hash is a fingerprint of the protocol definition and is used to detect changes in the protocol definition. See [Compatibility Validation Process in Detail](/docs/compatibility.md#compatibility-validation-process-in-detail) + +Then CRC-16 is calculated over the message data and compared to the last 2 bytes of the message. +If the CRC-16 is not correct, the message should be discarded. + diff --git a/docs/syntax.md b/docs/syntax.md new file mode 100644 index 0000000..e9d65c5 --- /dev/null +++ b/docs/syntax.md @@ -0,0 +1,221 @@ +# Syntax Documentation + +bproto uses a custom syntax `bproto` (file extension `.bproto`). +Each protocol is described in a single bproto file. + +For Beginners, a simple example of a protocol definition and usage can be found [here](/docs/quickstart.md). + +The protocol definitions consist of the following basic components: **messages**, **enums**, **bitfields**, and the **protocol tag line**. + +## Table of Contents +- [Syntax Documentation](#syntax-documentation) + - [Table of Contents](#table-of-contents) + - [Names and IDs](#names-and-ids) + - [Protocol Tag Line](#protocol-tag-line) + - [Message](#message) + - [Fields](#fields) + - [Datatypes](#datatypes) + - [Bitfields](#bitfields) + - [Enumerations](#enumerations) + - [Comments](#comments) + +## Names and IDs +Names of enums, bitfields, messages, and fields support any letter (lower and uppercase), numbers, and underscores. + +Regular expression: `[a-zA-Z0-9_]+` + +Numbers/IDs are decimal encoded numbers. Trailing zeros are ignored. + +Regular expression: `[0-9]+` + +## Protocol Tag Line +This is a short description of the protocol consisting of a name and a version number. +This must be the first line of any bproto definition file. + +**Syntax:** +```bproto +protocol version +``` +**Example:** +```bproto +protocol myCustomAirplaneProtocol version 2 +``` + +The name should identify the protocol in the implementation context (like `robotControlProtocol` or `weatherStationTelemetryDataStream`). +The version number is used to differentiate between different iterations of the protocol. Different versions of the same protocol are incompatible. + +For detailed information on naming conventions and versioning strategies, please refer to the [compatibility documentation](/docs/compatibility.md#naming-conventions). + +## Message + +A message is a collection of fields that can be serialized and deserialized to be sent over the wire or channel. +A message should only be a transport container and should not, if possible, be stored permanently. + +**Syntax:** +```bproto +message [] { + // List of fields + [] : , + [] : , + .... + [] : , +} +``` +**Example:** +```bproto +message [1] EngineSensorData { + [0] engine_thrust_left : float32, + [1] engine_thrust_right : float32, + [2] fuel_tanks_fill : uint32[8], // Fill levels of 8 fuel tanks + [3] engine_fault_left : bool, + [4] engine_fault_right : bool +} +``` + +- **Message ID:** Unique identifier within a protocol definition used to differentiate different messages from each other. The Message ID should be between 0 and 255. +- **Message Name:** Unique name given to this message. This will be used to identify a field in the protocol implementation. + +### Fields +A message can have as many fields as necessary. These are comparable to members in a class or fields in a C-struct. +A field has an `ID`, `Name`, and a `datatype`. + +**Syntax:** +`[] : ` +**Example:** +``[2] fanSpeed : float32`` + +- **Name:** This is a unique name given to the field. This will be used to identify a field in the protocol implementation. +- **ID:** A unique identifier used to order the fields in the encoding/decoding. More about serialization can be found [here](/docs/serialization.md#message-field-data-encoding). +- **datatype:** What type of data to store in this field. + +### Datatypes +Datatypes in bproto are independent of language and are translated into the target language-specific type. +Every Field in a message must have a datatype. + +Here is a list of datatypes available: + +| bproto Datatype | Encode Size (bytes) | Python | C | Value Range | Description | +| --------------- | ------------------- | ------- | ---------- | ------------------------------------------------------- | ---------------------------- | +| `uint8` | 1 | `int` | `uint8_t` | 0 to 255 | Unsigned 8-bit integer | +| `uint16` | 2 | `int` | `uint16_t` | 0 to 65,535 | Unsigned 16-bit integer | +| `uint32` | 4 | `int` | `uint32_t` | 0 to 4,294,967,295 | Unsigned 32-bit integer | +| `int64` | 8 | `int` | `uint64_t` | 0 to 18,446,744,073,709,551,615 | Unsigned 64-bit integer | +| `int8` | 1 | `int` | `int8_t` | -128 to 127 | Signed 8-bit integer | +| `int16` | 2 | `int` | `int16_t` | -32,768 to 32,767 | Signed 16-bit integer | +| `int32` | 4 | `int` | `int32_t` | -2,147,483,648 to 2,147,483,647 | Signed 32-bit integer | +| `int64` | 8 | `int` | `int64_t` | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | Signed 64-bit integer | +| `float32` | 4 | `float` | `float` | 1.2E-38 to 3.4E+38 | 32-bit floating point number | +| `float64` | 8 | `float` | `double` | 2.3E-308 to 1.7E+308 | 64-bit floating point number | +| `string[]` | N | `str` | `char[]` | N/A | Fixed-size string | +| `char` | 1 | `str` | `char` | Single character | Single character | +| `bool` | 1 | `bool` | `bool` | true or false | Boolean value | + +Every datatype in this list can be used in a fixed-size array by using this format: + +**Syntax:** +`[]` +**Example:** +`float[]` + +A special case is `string`. +Strings _must_ be declared as an array because their size must be known from the start. + +There are also `bits` and `enum` corresponding to **bitfields** and **enums**. +They are a bit special and are not able to be used as an array. +Bitfields and enums can be declared inline. +More details can be found in the sections about [enums](#enumerations) and [bitfields](#bitfields). + +How different datatypes are encoded can be found [here](/docs/serialization.md#message-field-data-encoding). + +## Bitfields +A compact representation of multiple boolean flags within a single byte or group of bytes, optimizing space for binary communication. +Bitfields are treated as datatypes. + +**Syntax:** +```bproto +bits { + <1st bit>, <2nd bit>, ... +} +``` +**Example:** +```bproto +bits engine_enables { + engine_left, engine_right +} +``` + +Each bit can be either 0 or 1 (true or false), represented as a bool in the target language. +More details on how Bitfields are encoded in the corresponding language can be found [here](/docs/datatype_language_details.md#bitfields). + +Bitfields, like Enumerations, can be declared inline in a [message](#message). + +**Syntax:** +```bproto +[] : bits { + <1st bit>, <2nd bit>, ... +} +``` +**Example:** +```bproto + [1] lightsEnable: bits { + Front, Spot, Key, BackLight + } +``` + +Bitfield naming when using them inline is found [here](/docs/datatype_language_details.md#inline). + +## Enumerations +A set of named integer values that represent discrete states or options. +Enums enhance code clarity by using descriptive names instead of raw numeric values. +Enumerations or Enums in bproto are comparable to C or Python enums. +More details on how Enums are encoded in the corresponding language can be found [here](/docs/datatype_language_details.md#enums). +Enums are treated as datatypes in bproto. + +Each Enum item represents a positive (or zero) numeric value. +These can be implicitly generated by bproto, incrementing by 1. +The value of the Enum value therefore depends on the declaration order. +This may lead to bugs and issues when updating one client/receiver's protocol but not the counterparts. +For more info about this problem, see [here](/docs/serialization.md#message-field-data-encoding). + +Enum values can also be set explicitly, which eliminates this problem. + +**Syntax:** +```bproto +enum { + (= ), + (= ), + ... + (= ), +} +``` +**Example:** +```bproto +dangerLvl: enum { + WARNING, + MELTDOWN, + ERROR = 10, +} +``` + +**Syntax:** +```bproto +[] : enum { + (= ), + (= ), + ... + (= ), +} +``` +**Example:** +```bproto + [6] dangerLvl: enum { + WARNING, + MELTDOWN = 5, + ERROR, + } +``` + +Enumeration naming when using them inline is found [here](/docs/datatype_language_details.md#inline). + +## Comments +bproto supports C-style comments (with `// ...` and `/* ... */`). diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..aaf0007 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +testpaths = + test/output + test/compiler diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a45ad01 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +antlr4-python3-runtime==4.13.2 +coverage==7.6.12 +exceptiongroup==1.2.2 +iniconfig==2.0.0 +Jinja2==3.1.5 +MarkupSafe==3.0.2 +packaging==24.2 +pluggy==1.5.0 +pytest==8.3.4 +pytest-cov==6.0.0 +tomli==2.2.1 diff --git a/run_docker_compiler.py b/run_docker_compiler.py new file mode 100755 index 0000000..a08e220 --- /dev/null +++ b/run_docker_compiler.py @@ -0,0 +1,50 @@ +#!/usr/bin/python3 +import os +import argparse + +cli_args_parser = argparse.ArgumentParser(description="Bproto Compiler") + +cli_args_parser.add_argument( + "-i", "--input", + help="Input file to compile", + required=True +) +cli_args_parser.add_argument( + "-o", "--output", + help="Output file to write compiled code to", + required=True +) +cli_args_parser.add_argument( + "-t", "--target", + help="Target language to compile to", + required=True, + choices=["python", "c", "txt"], + action="append" +) + +args = cli_args_parser.parse_args() + +targets = " ".join([ + f"--target \"{i}\"" + for i in args.target +]) + +input_basename = os.path.basename(args.input) +input_path = os.path.abspath(args.input) +output_path = os.path.abspath(args.output) + +if not os.path.isfile(input_path): + print(f"{input_path} is not a file") + exit(-1) + +if not os.path.isdir(output_path): + print(f"{output_path} is not folder") + exit(-1) + +c = "docker pull alexanderhd27/bproto-compiler:latest" +print(f"RUN: {c}") +os.system(c) + +c = f"docker run -v {output_path}:/compiler/data -v {input_path}:/compiler/{input_basename} --rm alexanderhd27/bproto-compiler:latest /compiler/src/main.py --output /compiler/data --input /compiler/{input_basename} {targets}" +print(f"RUN: {c}") +os.system(c) diff --git a/scripts/docker_and_push.bash b/scripts/docker_and_push.bash new file mode 100755 index 0000000..5fb6399 --- /dev/null +++ b/scripts/docker_and_push.bash @@ -0,0 +1,3 @@ +#!/usr/bin/bash +docker build . --tag alexanderhd27/bproto-compiler:latest +docker push alexanderhd27/bproto-compiler:latest \ No newline at end of file diff --git a/src/backend/__init__.py b/src/backend/__init__.py new file mode 100644 index 0000000..49ad204 --- /dev/null +++ b/src/backend/__init__.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod +from jinja2 import Environment +from protocol_components.protocol import ProtocolDefinitions +from backend.fsOutput import BackendFileSystemOutput + + +class BackendRenderer(ABC): + + @abstractmethod + def __init__(self, output_folder: str): + raise NotImplementedError() + + @abstractmethod + def render(self, protocol_definition: ProtocolDefinitions, jinja_env: Environment) -> BackendFileSystemOutput: + raise NotImplementedError() diff --git a/src/backend/fsOutput.py b/src/backend/fsOutput.py new file mode 100644 index 0000000..72096a5 --- /dev/null +++ b/src/backend/fsOutput.py @@ -0,0 +1,237 @@ +from abc import ABC, abstractmethod +from typing import Literal +import os +import re +import shutil +import jinja2 + +FILE_TREE_STRING_POSITION_t = Literal["last", "first", "mid"] + + +def map_link_symbol(position: FILE_TREE_STRING_POSITION_t) -> str: + """For displaying the file tree in the console, this function maps the position of the file in the tree to the correct link symbol. + + Args: + position (FILE_TREE_STRING_POSITION_t): The position of the file in the tree. + + Returns: + str: The correct link symbol for the position. + """ + match position: + case "last": + return "└" + case "first": + return "├" + case "mid": + return "├" + case _: # This should not happend + return "├" + + +class BackendFileSystemOutput(ABC): + """Represents a node in the filesystem output tree. + + This class is abstract and should be inherited by classes that represent files or folders in the filesystem output tree. + """ + INDENT_FILE_TREE_STRING = " " * 4 + + @abstractmethod + def saveToDisk(self, parent_path: str): + """Saves the file or folder to the disk. + Calles their potential subfiles and subfolders to save themselves. + + Args: + parent_path (str): Where to save the file or folder. + """ + pass + + @abstractmethod + def assemble_file_tree_string(self, indent: str = "", position: FILE_TREE_STRING_POSITION_t = "last") -> str: + """Assembles a string representation of the file tree. + This is ment to display to the use what the output will look like. + + Args: + layer (int, optional): Indentation layer. Defaults to "". Should be left untouched for the intial call. + position (FILE_TREE_STRING_POSITION_t, optional): For multiple children nodes, what position in the parent node. Defaults to "first". Should be left untouched for the intial call. + + Returns: + str: The string representation of the file tree (multiline). + """ + pass + + @abstractmethod + def toString(self) -> dict[str, str]: + """Renders out FilesystemOutput as a dictionary of strings. + Each key is a filename and the value is the content of the file. + Folders are include in the filename list with a trailing slash, but not as a separate key. + + Returns: + dict[str, str]: _description_ + """ + pass + + +class BackendFSOutFile(BackendFileSystemOutput): + """Represents a file with string conntent in the filesystem output tree. + + Inherited from BackendFileSystemOutput. + """ + def __init__(self, filename: str, conntent: str): + """Initializes the file with conntent and a filename. + + Args: + conntent (str): File conntent as string. + filename (str): Filename. + """ + self.conntent = conntent + self.filename = filename + + def saveToDisk(self, parent_path: str): + p = os.path.normpath(os.path.join(parent_path, self.filename)) + with open(p, "w") as f: + f.write(self.conntent) + + def toString(self) -> dict[str, str]: + return {self.filename: self.conntent} + + def assemble_file_tree_string(self, indent: str = "", position: FILE_TREE_STRING_POSITION_t = "last") -> str: + return indent + f"{map_link_symbol(position)}── [{self.filename}]\n" + + +class BackendFSOutFolder(BackendFileSystemOutput): + """Represents a folder in the filesystem output tree. + This Node has multiple subnodes, can be files or folders. + + Inherited from BackendFileSystemOutput. + """ + def __init__(self, name: str, sub_content: list[BackendFileSystemOutput] = []): + """Initializes the folder with a name and subnodes. + + Args: + name (str): Name of the folder. + sub_content (list[BackendFileSystemOutput]): Subnodes of the folder. + """ + self.name = name + self.sub_content: list[BackendFileSystemOutput] = sub_content + + def saveToDisk(self, parent_path: str): + p = os.path.normpath(os.path.join(parent_path, self.name)) + # This cause an **incident** + # if os.path.exists(p): + # shutil.rmtree(p) + + os.makedirs(p, exist_ok=True) + + for sub in self.sub_content: + sub.saveToDisk(p) + + def toString(self) -> dict[str, str]: + res = {} + + for sub in self.sub_content: + res.update({ + os.path.normpath(os.path.join(self.name, k)): v + for k, v in sub.toString().items() + }) + return res + + def assemble_file_tree_string(self, indent: str = "", position: FILE_TREE_STRING_POSITION_t = "last") -> str: + if position == "last": + indent_string = indent + BackendFileSystemOutput.INDENT_FILE_TREE_STRING + else: + indent_string = indent + ("│" + BackendFileSystemOutput.INDENT_FILE_TREE_STRING[1:]) + + s = indent + f"{map_link_symbol(position)}── {self.name}\n" + for i, sub in enumerate(self.sub_content): + if i == len(self.sub_content) - 1: + s += sub.assemble_file_tree_string(indent_string, "last") + else: + s += sub.assemble_file_tree_string(indent_string, "mid") + return s + + +class BackendFSOutStaticConent(BackendFileSystemOutput): + """Represents a static file or folder in the filesystem output tree. + + The content is copied from the source to the output. + + Inherited from BackendFileSystemOutput. + """ + def __init__(self, src_path: str, dest_path: str | None = None): + """Initializes the static content with a source path and a destination path. + + If dest_path is None, the top level folder (basename) of the source path is used. + + Args: + src_path (str): _description_ + dest_path (str | None, optional): _description_. Defaults to None. + """ + self.src_path = src_path + self.dest_path = dest_path or os.path.basename(src_path) + + def saveToDisk(self, parent_path: str): + dest = os.path.normpath(os.path.join(parent_path, self.dest_path)) + + # if os.path.exists(dest): + # shutil.rmtree(dest) + + shutil.copy(self.src_path, dest) + + def toString(self) -> dict[str, str]: + res = {} + if os.path.isfile(self.src_path): + with open(self.src_path, "r") as f: + res.update({ + os.path.normpath(os.path.join(self.dest_path, os.path.basename(self.src_path))): f.read() + }) + else: + for root, _, files in os.walk(self.src_path): + for file in files: + with open(os.path.normpath(os.path.join(root, file)), "r") as f: + res.update({ + os.path.normpath(os.path.join(self.dest_path, file)): f.read() + }) + + return res + + def assemble_file_tree_string(self, indent: str = "", position: FILE_TREE_STRING_POSITION_t = "last") -> str: + return indent + f"{map_link_symbol(position)}── Static [{self.dest_path}]\n" + + +class BackendFSOutputJinjaFile(BackendFSOutFile): + """Represents a file that is rendered with jinja2. + + Inherited from BackendFSOutFile. + """ + def __init__(self, jinja_env: jinja2.Environment, template_name: str, output_name: str | None, context: dict): + """Initializes the file with a jinja2 environment, a template name, an output name and a context. + + Args: + jinja_env (jinja2.Environment): Jinja2 environment to render the template. + template_name (str): Jinja2 template name. + output_name (str): Filename of the output; If left None, template_name is used and the .jinja2 extension is removed. + context (dict): Context for the template, needed for rendering. + """ + self.jinja_env = jinja_env + self.template_name = template_name + self.output_name = output_name + self.context = context + + if self.output_name is None: + self.output_name = re.sub(r"(?i)\b\.jinja\d?\b", "", self.template_name) + + def saveToDisk(self, parent_path: str): + template = self.jinja_env.get_template(self.template_name) + output = template.render(self.context) + + p = os.path.normpath(os.path.join(parent_path, self.output_name)) + with open(p, "w") as f: + f.write(output) + + def toString(self) -> dict[str, str]: + template = self.jinja_env.get_template(self.template_name) + output = template.render(self.context) + return {self.output_name: output} + + def assemble_file_tree_string(self, indent: str = "", position: FILE_TREE_STRING_POSITION_t = "last") -> str: + return indent + f"{map_link_symbol(position)}── Template [{self.output_name}]\n" diff --git a/src/backend/rendering/cBackend.py b/src/backend/rendering/cBackend.py new file mode 100644 index 0000000..66036d8 --- /dev/null +++ b/src/backend/rendering/cBackend.py @@ -0,0 +1,270 @@ +from backend import BackendRenderer +from protocol_components.dtypes import BprotoFieldBaseType +from protocol_components.protocol import ProtocolDefinitions +from protocol_components.bitfields import Bitfield +from protocol_components.field import Field, FieldBitfield, FieldEnum +from protocol_components.crc import CRC_SIZE + +from backend.fsOutput import BackendFileSystemOutput, BackendFSOutputJinjaFile, BackendFSOutFolder, BackendFSOutStaticConent +from nameHandling.style.cNameStyle import NameStyleC +from nameHandling.base import ComponentName + +from jinja2 import Environment +from copy import deepcopy + + +C_DATA_TYPE_MAP = { + BprotoFieldBaseType.BOOL: ("bool", False), + + BprotoFieldBaseType.UINT8: ("uint8_t", 0), + BprotoFieldBaseType.UINT16: ("uint16_t", 0), + BprotoFieldBaseType.UINT32: ("uint32_t", 0), + BprotoFieldBaseType.UINT64: ("uint64_t", 0), + + BprotoFieldBaseType.INT8: ("int8_t", 0), + BprotoFieldBaseType.INT16: ("int16_t", 0), + BprotoFieldBaseType.INT32: ("int32_t", 0), + BprotoFieldBaseType.INT64: ("int64_t", 0), + + BprotoFieldBaseType.FLOAT32: ("float", 0.0), + BprotoFieldBaseType.FLOAT64: ("double", 0.0), + + BprotoFieldBaseType.CHAR: ("char", "''"), + BprotoFieldBaseType.STRING: ("char", "\"\""), + + BprotoFieldBaseType.BITFIELD: ("", ""), + BprotoFieldBaseType.ENUM: ("", ""), +} + +FOR_LOOP_THRESHOLD = 8 + + +def map_data_type(field: Field) -> str: + if isinstance(field, FieldBitfield): + return { + "name": NameStyleC.toStr(field.bitfield.name, "bitfield_name"), + "aux": None, + "array": "" + } + + elif isinstance(field, FieldEnum): + return { + "name": NameStyleC.toStr(field.enum.name, "enum_name"), + "aux": C_DATA_TYPE_MAP[field.enum.map_auxiliary_datatypes()][0], + "array": "" + } + + elif field.type in BprotoFieldBaseType: + if field.array_size > 1 or field.type == BprotoFieldBaseType.STRING: + return { + "name": C_DATA_TYPE_MAP[field.type][0], + "aux": None, + "array": f"[{field.array_size}]" + } + else: + return { + "name": C_DATA_TYPE_MAP[field.type][0], + "aux": None, + "array": "" + } + + else: + raise ValueError(f"Type {field.type} is not implement in map_Data_type()") + + +def convert_bitfield_to_c_style_bit_list(bitfield: Bitfield) -> list[tuple[str, int]]: + last_pos = 0 + + res = [] + + for bit_name, pos in bitfield.bits.items(): + if pos - (last_pos + 1) > 1: + # We skiped bits + res.append({ + "name": "", + "length": pos - (last_pos + 1) - 1, + "spacer": True, + }) + last_pos = pos + + res.append({ + "name": NameStyleC.toStr(bit_name, "bitfield_member"), + "length": 1, + "spacer": False, + "bit_pos": pos % 8, + "byte_pos": pos // 8 + }) + + return res + + +def generate_max_message_struct_expr(p: ProtocolDefinitions) -> str: + res = "{}" + + for i in p.messages.values(): + res = res.format("MAX({}, sizeof(" + NameStyleC.toStr(i.name, "struct_name") + "))") + return res.format(0) + + +def convert_field_to_toBytes_overwrite(field: Field): + if isinstance(field, FieldBitfield): + return f"toBytes_{NameStyleC.toStr(field.bitfield.name, 'bitfield_name')}(data, &msg->{NameStyleC.toStr(field.name, 'struct_member')})" + + else: + return None + + +def convert_field_to_fromBytes_overwrite(field: Field): + if isinstance(field, FieldBitfield): + return f"fromBytes_{NameStyleC.toStr(field.bitfield.name, 'bitfield_name')}(data, &msg->{NameStyleC.toStr(field.name, 'struct_member')})" + + else: + return None + + +class CBackendRenderer(BackendRenderer): + + def __init__(self, output_folder): + self.output_folder = output_folder + + self.protocol: ProtocolDefinitions = None + self.jinja_env: Environment = None + self.jinja_context: dict = None + + def render(self, + protocol_definition: ProtocolDefinitions, + jinja_env: Environment) -> BackendFileSystemOutput: + + if protocol_definition.protocol_hash_inital is None: + raise ValueError("Protocol hash inital for crc16 should no be None") + + self.protocol = NameStyleC.preprocess(deepcopy(protocol_definition)) + + self.jinja_env = jinja_env + self.jinja_context = { + "protocol": { + "name": NameStyleC.toStr(self.protocol.name, "enum_item"), + "version": self.protocol.version, + "crc_size": CRC_SIZE, + "crc_initial": hex(self.protocol.protocol_hash_inital), + "max_struct_size_expr": generate_max_message_struct_expr(self.protocol), + "max_data_size": max([i.get_size_bytes() for i in self.protocol.messages.values()]) + 1 + CRC_SIZE + }, + "import_name": NameStyleC.toStr(self.protocol.name, 'enum_item'), + "enums": [ + { + "name": NameStyleC.toStr(name, "enum_name"), + "consts": [ + (NameStyleC.toStr(k, "enum_item"), v) + for k, v in e.values.items() + ] + } + for name, e in self.protocol.enums.items() + ], + "bitfields": [ + { + "name": NameStyleC.toStr(b.name, "bitfield_name"), + "size": b.get_size_bytes(), + "size_name": NameStyleC.toStr(b.name, "enum_item").upper() + "_SIZE", + "bits": convert_bitfield_to_c_style_bit_list(b), + } + for name, b in self.protocol.bitfields.items() + ], + "messages": [ + { + "name": NameStyleC.toStr(name, "struct_name"), + "id": m.get_identifier(), + "id_name": NameStyleC.toStr(name, "enum_item") + "_ID", + "size": m.get_size_bytes() + 1 + CRC_SIZE, + "size_name": NameStyleC.toStr(name, "enum_item") + "_SIZE", + "fields": [ + { + "name": NameStyleC.toStr(k, "struct_member"), + "size": v.get_size_bytes(), + "base_size": v.get_base_size_bytes(), + "array_size": v.array_size, + "decompose_mode": ( + "for" + if v.array_size > FOR_LOOP_THRESHOLD or v.type == BprotoFieldBaseType.STRING + else ( + "inline_for" if v.array_size > 1 else "normal" + ) + ), + "type": map_data_type(v), + "pre_init_value": 0 if isinstance(v, FieldEnum) else None, + "toBytes_overwride": convert_field_to_toBytes_overwrite(v), + "fromBytes_overwride": convert_field_to_fromBytes_overwrite(v) + } + for k, v in m.fields.items() + ] + } + for name, m, in self.protocol.messages.items() + ] + } + + return BackendFSOutFolder(self.output_folder, [ + BackendFSOutputJinjaFile( # CMake + self.jinja_env, + "c/template/bproto.cmake.jinja2", + f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}.cmake", + self.jinja_context + ), + BackendFSOutFolder("include", [ + BackendFSOutputJinjaFile( # Enum + self.jinja_env, + "c/template/include/enums.h.jinja2", + f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_enum.h", + self.jinja_context + ), + BackendFSOutputJinjaFile( # Message + self.jinja_env, + "c/template/include/message/message.h.jinja2", + f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_message.h", + self.jinja_context + ), + BackendFSOutputJinjaFile( # Bitfields + self.jinja_env, + "c/template/include/bitfield/bitfield.h.jinja2", + f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_bitfield.h", + self.jinja_context + ), + BackendFSOutputJinjaFile( # Bitfields + self.jinja_env, + "c/template/include/crc.h.jinja2", + f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_crc.h", + self.jinja_context + ), + ]), + BackendFSOutFolder("src", [ + BackendFSOutputJinjaFile( # Bitfields + self.jinja_env, + "c/template/src/bitfield.c.jinja2", + f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_bitfield.c", + self.jinja_context + ), + BackendFSOutputJinjaFile( # Message fromBytes + self.jinja_env, + "c/template/src/message/fromBytes.c.jinja2", + f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_message_fromBytes.c", + self.jinja_context + ), + BackendFSOutputJinjaFile( # Message toBytes + self.jinja_env, + "c/template/src/message/toBytes.c.jinja2", + f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_message_toBytes.c", + self.jinja_context + ), + BackendFSOutputJinjaFile( # CRC + self.jinja_env, + "c/template/src/crc.c.jinja2", + f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_crc.c", + self.jinja_context + ), + BackendFSOutputJinjaFile( # Map Message Size/ID + self.jinja_env, + "c/template/src/message/mapping.c.jinja2", + f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_mapping.c", + self.jinja_context + ), + ]), + ]) diff --git a/src/backend/rendering/pythonBackend.py b/src/backend/rendering/pythonBackend.py new file mode 100644 index 0000000..faeef5d --- /dev/null +++ b/src/backend/rendering/pythonBackend.py @@ -0,0 +1,244 @@ +from backend import BackendRenderer +from protocol_components.dtypes import BprotoFieldBaseType +from protocol_components.protocol import ProtocolDefinitions +from protocol_components.field import Field, FieldBitfield, FieldEnum +from protocol_components.crc import CRC_SIZE + +from backend.fsOutput import BackendFileSystemOutput, BackendFSOutputJinjaFile, BackendFSOutFolder, BackendFSOutStaticConent +from nameHandling.style.pythonNameStyle import NameStylePython +from nameHandling.base import ComponentName + +from jinja2 import Environment +from copy import deepcopy + +ENDIANDNESS = "<" + +PYTHON_DATA_TYPE_MAP = { + BprotoFieldBaseType.BOOL: ("bool", False), + + BprotoFieldBaseType.UINT8: ("int", 0), + BprotoFieldBaseType.UINT16: ("int", 0), + BprotoFieldBaseType.UINT32: ("int", 0), + BprotoFieldBaseType.UINT64: ("int", 0), + + BprotoFieldBaseType.INT8: ("int", 0), + BprotoFieldBaseType.INT16: ("int", 0), + BprotoFieldBaseType.INT32: ("int", 0), + BprotoFieldBaseType.INT64: ("int", 0), + + BprotoFieldBaseType.FLOAT32: ("float", 0.0), + BprotoFieldBaseType.FLOAT64: ("float", 0.0), + + BprotoFieldBaseType.CHAR: ("str", "''"), + BprotoFieldBaseType.STRING: ("str", "''"), + + BprotoFieldBaseType.BITFIELD: ("", ""), + BprotoFieldBaseType.ENUM: ("", ""), +} + +PYTHON_DATA_TYPE_STRUCT_FORMAT = { + BprotoFieldBaseType.BOOL: "B", + + BprotoFieldBaseType.UINT8: "B", + BprotoFieldBaseType.UINT16: "H", + BprotoFieldBaseType.UINT32: "I", + BprotoFieldBaseType.UINT64: "Q", + + BprotoFieldBaseType.INT8: "b", + BprotoFieldBaseType.INT16: "h", + BprotoFieldBaseType.INT32: "i", + BprotoFieldBaseType.INT64: "q", + + BprotoFieldBaseType.FLOAT32: "f", + BprotoFieldBaseType.FLOAT64: "d", +} + + +def map_to_bytes_conversion(field: Field, endiend: str) -> str: + name = NameStylePython.toStr(field.name, "class_member") + + if isinstance(field, FieldEnum): + return f"struct.pack('{endiend}B', self.{name}.value)" + + elif isinstance(field, FieldBitfield): + return f"self.{name}.to_bytes()" + + elif field.type in PYTHON_DATA_TYPE_STRUCT_FORMAT: + if field.array_size > 1: + argument = ", ".join([ + f"self.{name}[{i}]" + for i in range(field.array_size) + ]) + return f"struct.pack('{endiend}{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type] * field.array_size}', {argument})" + else: + return f"struct.pack('{endiend}{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type]}', self.{name})" + + elif field.type == BprotoFieldBaseType.STRING or field.type == BprotoFieldBaseType.CHAR: + return f"self.{name}.encode('ascii')[:{field.array_size}].ljust({field.array_size}, b'\\x00')" + + else: + raise TypeError(f"Cannot convert field {field} to python to bytes conversion string") + + +def map_from_bytes_conversion(field: Field, endiend: str) -> str: + if isinstance(field, FieldEnum): + return f"{NameStylePython.toStr(field.enum.name, "enum_name")}(struct.unpack('{endiend}B', data[:{field.get_size_bytes()}])[0])" + + elif isinstance(field, FieldBitfield): + return f"{NameStylePython.toStr(field.bitfield.name, "class_name")}().from_bytes(data[:{field.get_size_bytes()}])" + + elif field.type == BprotoFieldBaseType.STRING or field.type == BprotoFieldBaseType.CHAR: + return f"data[:{field.get_size_bytes()}].decode('ascii').strip('\\x00')" + + elif field.type == BprotoFieldBaseType.BOOL: + if field.array_size > 1: + return f"[bool(i) for i in struct.unpack('{endiend}{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type] * field.array_size}', data[:{field.get_size_bytes()}])]" + else: + return f"bool(struct.unpack('>{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type]}', data[:{field.get_size_bytes()}])[0])" + + elif field.type in PYTHON_DATA_TYPE_STRUCT_FORMAT: + if field.array_size > 1: + return f"list(struct.unpack('{endiend}{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type] * field.array_size}', data[:{field.get_size_bytes()}]))" + else: + return f"struct.unpack('{endiend}{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type]}', data[:{field.get_size_bytes()}])[0]" + + else: + raise TypeError(f"Cannot convert field {field} to python to bytes conversion string") + + +def map_data_type_anotation(field: Field) -> str: + if isinstance(field, FieldEnum): + return f"{NameStylePython.toStr(field.enum.name, "enum_name")}" + + elif isinstance(field, FieldBitfield): + return f"{NameStylePython.toStr(field.bitfield.name, "class_name")}" + + elif field.type in PYTHON_DATA_TYPE_STRUCT_FORMAT: + if field.array_size > 1: + return f"Annotated[List[{PYTHON_DATA_TYPE_MAP[field.type][0]}], {field.array_size}]" + else: + return f"{PYTHON_DATA_TYPE_MAP[field.type][0]}" + + elif field.type == BprotoFieldBaseType.STRING or field.type == BprotoFieldBaseType.CHAR: + return "str" + + else: + raise TypeError(f"Cannot convert field {field} to python to bytes conversion string") + + +class PythonBackendRenderer(BackendRenderer): + + def __init__(self, output_folder): + self.output_folder = output_folder + + self.protocol: ProtocolDefinitions = None + self.jinja_env: Environment = None + self.jinja_context: dict = None + + def render(self, + protocol_definition: ProtocolDefinitions, + jinja_env: Environment) -> BackendFileSystemOutput: + + if protocol_definition.protocol_hash_inital is None: + raise ValueError("Protocol hash inital for crc16 should no be None") + + self.protocol = NameStylePython.preprocess(deepcopy(protocol_definition)) + + self.jinja_env = jinja_env + self.jinja_context = { + "protocol": { + "name": NameStylePython.toStr(self.protocol.name, "enum_item"), + "version": self.protocol.version, + "crc_size": CRC_SIZE, + "crc_initial": hex(self.protocol.protocol_hash_inital), + "endian_format": ENDIANDNESS, + "endian_str": "little" if ENDIANDNESS == "<" else "big" + }, + "import_name": NameStylePython.toStr(self.protocol.name, 'enum_item'), + "enums": [ + { + "name": NameStylePython.toStr(name, "enum_name"), + "consts": [ + (NameStylePython.toStr(k, "enum_item"), v) + for k, v in e.values.items() + ] + } + for name, e in self.protocol.enums.items() + ], + "bitfields": [ + { + "name": NameStylePython.toStr(b.name, "class_name"), + "size": b.get_size_bytes(), + "bits": [ + { + "name": NameStylePython.toStr(k, "class_member"), + "pos": v + } + for k, v in b.bits.items() + ] + } + for name, b in self.protocol.bitfields.items() + ], + "messages": [ + { + "name": NameStylePython.toStr(name, "class_name"), + "id": m.get_identifier(), + "id_name": NameStylePython.toStr(ComponentName(["MSG", "ID"]) + name, "enum_item"), + "size": m.get_size_bytes() + 1 + CRC_SIZE, + "fields": [ + { + "name": NameStylePython.toStr(k, "class_member"), + "default_value": NameStylePython.toStr(v.bitfield.name, "class_name") + "()" + if v.type == BprotoFieldBaseType.BITFIELD + else ( + NameStylePython.toStr(v.enum.name, "enum_name") + "." + NameStylePython.toStr(list(v.enum.values.keys())[0], "enum_item") + if v.type == BprotoFieldBaseType.ENUM + else PYTHON_DATA_TYPE_MAP[v.type][1] + ), + "size": v.get_size_bytes(), + "array_size": v.array_size, + "type": map_data_type_anotation(v), + "to_bytes_conversion": map_to_bytes_conversion(v, ENDIANDNESS), + "from_bytes_conversion": map_from_bytes_conversion(v, ENDIANDNESS) + } + for k, v in m.fields.items() + ] + } + for name, m, in self.protocol.messages.items() + ] + } + + return BackendFSOutFolder(self.output_folder, [ + BackendFSOutputJinjaFile( # Enum + self.jinja_env, + "python/template/enum.py.jinja2", + f"{NameStylePython.toStr(self.protocol.name, 'enum_item')}_protocol_enum.py", + self.jinja_context + ), + BackendFSOutputJinjaFile( # Bitfields + self.jinja_env, + "python/template/bproto_protocol_bitfield.py.jinja2", + f"{NameStylePython.toStr(self.protocol.name, 'enum_item')}_protocol_bitfield.py", + self.jinja_context + ), + BackendFSOutputJinjaFile( # Message IDs + self.jinja_env, + "python/template/bproto_protocol_packets_ids.py.jinja2", + f"{NameStylePython.toStr(self.protocol.name, 'enum_item')}_protocol_message_ids.py", + self.jinja_context + ), + BackendFSOutputJinjaFile( # Message + self.jinja_env, + "python/template/bproto_protocol_packets.py.jinja2", + f"{NameStylePython.toStr(self.protocol.name, 'enum_item')}_protocol_packets.py", + self.jinja_context + ), + BackendFSOutStaticConent( + "template/python/static/bproto_base.py", + "bproto_base.py" + ), + BackendFSOutStaticConent( + "template/python/static/bproto_error.py", + "bproto_error.py" + ) + ]) diff --git a/src/backend/rendering/txtBackend.py b/src/backend/rendering/txtBackend.py new file mode 100644 index 0000000..285e3d2 --- /dev/null +++ b/src/backend/rendering/txtBackend.py @@ -0,0 +1,71 @@ +from backend import BackendRenderer +from jinja2 import Environment +from protocol_components.dtypes import BprotoFieldBaseType +from protocol_components.protocol import ProtocolDefinitions +from backend.fsOutput import BackendFileSystemOutput, BackendFSOutputJinjaFile, BackendFSOutFolder +from nameHandling.base import NameStyleBproto + + +class TxtBackendRenderer(BackendRenderer): + + def __init__(self, output_folder): + self.output_folder = output_folder + + def render(self, + protocol_definition: ProtocolDefinitions, + jinja_env: Environment) -> BackendFileSystemOutput: + + return BackendFSOutFolder(self.output_folder, [ + BackendFSOutputJinjaFile( + jinja_env, + "txt/protocolSummary.txt.jinja2", + "protocolSummary.txt", context={ + "name": NameStyleBproto.toStr(protocol_definition.name), + "version": protocol_definition.version, + "enums": [ + { + "name": NameStyleBproto.toStr(name), + "consts": [ + (NameStyleBproto.toStr(k, "enum"), v) + for k, v in e.values.items() + ] + } + for name, e in protocol_definition.enums.items() + ], + "bitfields": [ + { + "name": NameStyleBproto.toStr(name), + "fields": [ + (NameStyleBproto.toStr(k, "member"), v) + for k, v in b.bits.items() + ] + } + for name, b in protocol_definition.bitfields.items() + ], + "messages": [ + { + "name": NameStyleBproto.toStr(name), + "id": m.get_identifier(), + "size": m.get_size_bytes(), + "fields": [ + { + "name": NameStyleBproto.toStr(k, "member"), + "type": v.type.value, + "size": v.get_size_bytes(), + "array_size": v.array_size, + "ref": NameStyleBproto.toStr(v.bitfield.name) + if v.type == BprotoFieldBaseType.BITFIELD + else ( + NameStyleBproto.toStr(v.enum.name) + if v.type == BprotoFieldBaseType.ENUM + else None + ) + } + for k, v in m.fields.items() + ] + } + for name, m, in protocol_definition.messages.items() + ] + } + )] + ) diff --git a/src/compiler.py b/src/compiler.py new file mode 100644 index 0000000..d19e982 --- /dev/null +++ b/src/compiler.py @@ -0,0 +1,54 @@ +from backend import BackendRenderer +from backend.rendering.txtBackend import TxtBackendRenderer +from backend.fsOutput import BackendFileSystemOutput, BackendFSOutFolder + +from protocol_components.crc import crc16_calc_inital +from protocol_components.protocol import ProtocolDefinitions + +from parser.parser import parse_ast_string, bproto_ErrorListener +from parser.ast_visitor import BprotoASTVisitor + +from jinja2 import FileSystemLoader, Environment +import sys + + +class BprotoCompiler: + + def __init__(self, backends: list[BackendRenderer, str], template_directory: str): + self.backends = backends + self.template_dir = template_directory + + def compile(self, source_text: str, main_output_folder: str) -> BackendFSOutFolder: + # Stage 1 - Parse & Create AST + ast = parse_ast_string(source_text) + + if type(ast) is bproto_ErrorListener: + print( + ast.get_syntax_error_message(source_text), + file=sys.stderr + ) + sys.exit(1) + + # Stage 2 - Interpret AST **Magic** and build ProtocolDefinition Object + # Also this does the forward and backward resolving + vinterp = BprotoASTVisitor() + protocol_definition: ProtocolDefinitions = vinterp.visit(ast) + + # Stage 3.1 - Render Txt for Hash + jinja_env = Environment( + loader=FileSystemLoader(self.template_dir) + ) + + txt_backend = TxtBackendRenderer(".") + txt_res = txt_backend.render(protocol_definition, jinja_env).toString().get("protocolSummary.txt") + protocol_definition.protocol_hash_inital = crc16_calc_inital(txt_res.encode("ascii")) + + # Stage 3.2 - Backend Rendering + backend_outputs: list[BackendFileSystemOutput] = [] + + for backend in self.backends: + backend_outputs.append( + backend.render(protocol_definition, jinja_env) + ) + + return BackendFSOutFolder(main_output_folder, backend_outputs) diff --git a/src/errors.py b/src/errors.py new file mode 100644 index 0000000..f2b4837 --- /dev/null +++ b/src/errors.py @@ -0,0 +1,113 @@ +from protocol_components import AbstractProtocolComponent + + +class BprotoCompilerError(Exception): + """A General error that is raised during the compilation stage. + + Inherited from Exception. + """ + + def __init__(self, message: str): + super().__init__(message) + + +class BprotoFrontendError(BprotoCompilerError): + """Error raised during the frontend compilation stage. + + Inherited from BprotoCompilerError. + """ + def __init__(self, message): + super().__init__(message) + + +class BprotoAlreadyDefinedError(BprotoFrontendError): + """Error that indicates that a protocol component in the same category/scope with the same name is already defined. + This error should be use during the frontend compilation state. + + Inherited from BprotoFrontendError. + """ + def __init__(self, entity: AbstractProtocolComponent): + self.entity = entity + super().__init__(f"{self.entity.get_type_name()} {self.entity.get_name()} already defined!") + + +class BprotoDuplicateNameError(BprotoFrontendError): + """Error that an item with an a entity with the same name is already defined in the same scope. + This error should be use during the frontend compilation state. + + Its very similar to BprotoAlreadyDefinedError, but this error is used for items that are not protocol components like fields or enum values + + Inherited from BprotoFrontendError. + """ + def __init__(self, item: str, parent_entity: AbstractProtocolComponent): + self.item = item + self.parent = parent_entity + super().__init__(f"{item} already defined in {parent_entity.get_type_name()} {parent_entity.get_name()}!") + + +class BprotoDuplicateEnumValueError(BprotoFrontendError): + """Error raise when building an enumeration and a value is used more than once. + This error should be use during the frontend compilation state. + + Incomparision to BprotoDuplicateNameError, this error is used for enum values, not the name. + Its specific to the enum component. + + Inherited from BprotoFrontendError. + """ + + def __init__(self, value: int, enum: AbstractProtocolComponent): + self.value = value + self.enum_name = enum + super().__init__(f"Value {value} already used in enum {enum.get_name()}!") + + +class BprotoDuplicateBitfieldPositionError(BprotoFrontendError): + """Error raise when building a bitfield and a position is used more than once. + This error should be use during the frontend compilation + + Incomparision to BprotoDuplicateNameError, this error is used for bitfield positions, not the name. + Its specific to the bitfield component. + + Inherited from BprotoFrontendError. + """ + def __init__(self, position: int, bitfield: AbstractProtocolComponent): + self.position = position + self.bitfield_name = bitfield + super().__init__(f"Position {position} already used in bitfield {bitfield.get_name()}!") + + +class BprotoEnumBitsizeTooLargeError(BprotoFrontendError): + """Error raise when building an enumeration and the size of the enum exceeds the size of 8 bytes. + This error should be use during the frontend compilation state. + + This is cause by the fact that bproto enums are just unsigned integer and the largest unsigned integer is 8 bytes long. + + Inherited from BprotoFrontendError. + """ + def __init__(self, enum: AbstractProtocolComponent): + self.enum = enum + super().__init__(f"Enum {enum.get_name()} exceed size of 8 bytes (= uint64) with {enum.get_size_bytes()} bytes!") + + +class BprotoMessageIDAlreadyUsed(BprotoFrontendError): + """Error raise when building a message and the message id is already used. + This error should be use during the frontend compilation state. + + Inherited from BprotoFrontendError. + """ + def __init__(self, message_id: int, message: AbstractProtocolComponent): + self.message_id = message_id + self.message = message + super().__init__(f"Message id {message_id} already used in {message.get_type_name()} {message.get_name()}!") + + +class BprotoUnresolvedReferenceError(BprotoFrontendError): + """Error raise when a reference to a protocol component could not be resolved. + This error should be use during the frontend compilation state. + + Inherited from BprotoFrontendError. + """ + def __init__(self, reference: str, parent: AbstractProtocolComponent, grand_parent: AbstractProtocolComponent): + self.reference = reference + self.parent = parent + super().__init__(f"Reference {reference} in {parent.get_type_name()} {parent.get_name()} (in {grand_parent.get_type_name()} {grand_parent.get_name()}) could not be resolved!") diff --git a/src/gen/bprotoV1.interp b/src/gen/bprotoV1.interp new file mode 100644 index 0000000..2303d61 --- /dev/null +++ b/src/gen/bprotoV1.interp @@ -0,0 +1,62 @@ +token literal names: +null +'protocol' +'version' +'message' +'{' +',' +'}' +'[' +']' +'enum' +'bits' +':' +'=' +null +null +null +null +null + +token symbolic names: +null +null +null +null +null +null +null +null +null +null +null +null +null +INT +IDENTIFIER +WS +COMMENT +COMMENT_MULTILINE + +rule names: +protocol_defintion +protocol_header +message_def +message_id +enum_def +bit_field_def +field +field_pos +dtype +type_standard +type_enum +type_bitfield +array_extension +enum_body +enum_field +bitfield_body +bitfield_field + + +atn: +[4, 1, 17, 156, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 1, 0, 1, 0, 1, 0, 1, 0, 4, 0, 39, 8, 0, 11, 0, 12, 0, 40, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 5, 2, 55, 8, 2, 10, 2, 12, 2, 58, 9, 2, 1, 2, 1, 2, 3, 2, 62, 8, 2, 3, 2, 64, 8, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 3, 8, 92, 8, 8, 1, 9, 1, 9, 3, 9, 96, 8, 9, 1, 10, 1, 10, 1, 10, 3, 10, 101, 8, 10, 1, 11, 1, 11, 1, 11, 3, 11, 106, 8, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 5, 13, 116, 8, 13, 10, 13, 12, 13, 119, 9, 13, 1, 13, 1, 13, 3, 13, 123, 8, 13, 3, 13, 125, 8, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 3, 14, 132, 8, 14, 1, 15, 1, 15, 1, 15, 1, 15, 5, 15, 138, 8, 15, 10, 15, 12, 15, 141, 9, 15, 1, 15, 1, 15, 3, 15, 145, 8, 15, 3, 15, 147, 8, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 3, 16, 154, 8, 16, 1, 16, 0, 0, 17, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 0, 0, 157, 0, 34, 1, 0, 0, 0, 2, 42, 1, 0, 0, 0, 4, 47, 1, 0, 0, 0, 6, 67, 1, 0, 0, 0, 8, 71, 1, 0, 0, 0, 10, 75, 1, 0, 0, 0, 12, 79, 1, 0, 0, 0, 14, 84, 1, 0, 0, 0, 16, 91, 1, 0, 0, 0, 18, 93, 1, 0, 0, 0, 20, 97, 1, 0, 0, 0, 22, 102, 1, 0, 0, 0, 24, 107, 1, 0, 0, 0, 26, 111, 1, 0, 0, 0, 28, 128, 1, 0, 0, 0, 30, 133, 1, 0, 0, 0, 32, 150, 1, 0, 0, 0, 34, 38, 3, 2, 1, 0, 35, 39, 3, 4, 2, 0, 36, 39, 3, 8, 4, 0, 37, 39, 3, 10, 5, 0, 38, 35, 1, 0, 0, 0, 38, 36, 1, 0, 0, 0, 38, 37, 1, 0, 0, 0, 39, 40, 1, 0, 0, 0, 40, 38, 1, 0, 0, 0, 40, 41, 1, 0, 0, 0, 41, 1, 1, 0, 0, 0, 42, 43, 5, 1, 0, 0, 43, 44, 5, 14, 0, 0, 44, 45, 5, 2, 0, 0, 45, 46, 5, 13, 0, 0, 46, 3, 1, 0, 0, 0, 47, 48, 5, 3, 0, 0, 48, 49, 3, 6, 3, 0, 49, 50, 5, 14, 0, 0, 50, 56, 5, 4, 0, 0, 51, 52, 3, 12, 6, 0, 52, 53, 5, 5, 0, 0, 53, 55, 1, 0, 0, 0, 54, 51, 1, 0, 0, 0, 55, 58, 1, 0, 0, 0, 56, 54, 1, 0, 0, 0, 56, 57, 1, 0, 0, 0, 57, 63, 1, 0, 0, 0, 58, 56, 1, 0, 0, 0, 59, 61, 3, 12, 6, 0, 60, 62, 5, 5, 0, 0, 61, 60, 1, 0, 0, 0, 61, 62, 1, 0, 0, 0, 62, 64, 1, 0, 0, 0, 63, 59, 1, 0, 0, 0, 63, 64, 1, 0, 0, 0, 64, 65, 1, 0, 0, 0, 65, 66, 5, 6, 0, 0, 66, 5, 1, 0, 0, 0, 67, 68, 5, 7, 0, 0, 68, 69, 5, 13, 0, 0, 69, 70, 5, 8, 0, 0, 70, 7, 1, 0, 0, 0, 71, 72, 5, 9, 0, 0, 72, 73, 5, 14, 0, 0, 73, 74, 3, 26, 13, 0, 74, 9, 1, 0, 0, 0, 75, 76, 5, 10, 0, 0, 76, 77, 5, 14, 0, 0, 77, 78, 3, 30, 15, 0, 78, 11, 1, 0, 0, 0, 79, 80, 3, 14, 7, 0, 80, 81, 5, 14, 0, 0, 81, 82, 5, 11, 0, 0, 82, 83, 3, 16, 8, 0, 83, 13, 1, 0, 0, 0, 84, 85, 5, 7, 0, 0, 85, 86, 5, 13, 0, 0, 86, 87, 5, 8, 0, 0, 87, 15, 1, 0, 0, 0, 88, 92, 3, 18, 9, 0, 89, 92, 3, 20, 10, 0, 90, 92, 3, 22, 11, 0, 91, 88, 1, 0, 0, 0, 91, 89, 1, 0, 0, 0, 91, 90, 1, 0, 0, 0, 92, 17, 1, 0, 0, 0, 93, 95, 5, 14, 0, 0, 94, 96, 3, 24, 12, 0, 95, 94, 1, 0, 0, 0, 95, 96, 1, 0, 0, 0, 96, 19, 1, 0, 0, 0, 97, 100, 5, 9, 0, 0, 98, 101, 3, 26, 13, 0, 99, 101, 5, 14, 0, 0, 100, 98, 1, 0, 0, 0, 100, 99, 1, 0, 0, 0, 101, 21, 1, 0, 0, 0, 102, 105, 5, 10, 0, 0, 103, 106, 3, 30, 15, 0, 104, 106, 5, 14, 0, 0, 105, 103, 1, 0, 0, 0, 105, 104, 1, 0, 0, 0, 106, 23, 1, 0, 0, 0, 107, 108, 5, 7, 0, 0, 108, 109, 5, 13, 0, 0, 109, 110, 5, 8, 0, 0, 110, 25, 1, 0, 0, 0, 111, 117, 5, 4, 0, 0, 112, 113, 3, 28, 14, 0, 113, 114, 5, 5, 0, 0, 114, 116, 1, 0, 0, 0, 115, 112, 1, 0, 0, 0, 116, 119, 1, 0, 0, 0, 117, 115, 1, 0, 0, 0, 117, 118, 1, 0, 0, 0, 118, 124, 1, 0, 0, 0, 119, 117, 1, 0, 0, 0, 120, 122, 3, 28, 14, 0, 121, 123, 5, 5, 0, 0, 122, 121, 1, 0, 0, 0, 122, 123, 1, 0, 0, 0, 123, 125, 1, 0, 0, 0, 124, 120, 1, 0, 0, 0, 124, 125, 1, 0, 0, 0, 125, 126, 1, 0, 0, 0, 126, 127, 5, 6, 0, 0, 127, 27, 1, 0, 0, 0, 128, 131, 5, 14, 0, 0, 129, 130, 5, 12, 0, 0, 130, 132, 5, 13, 0, 0, 131, 129, 1, 0, 0, 0, 131, 132, 1, 0, 0, 0, 132, 29, 1, 0, 0, 0, 133, 139, 5, 4, 0, 0, 134, 135, 3, 32, 16, 0, 135, 136, 5, 5, 0, 0, 136, 138, 1, 0, 0, 0, 137, 134, 1, 0, 0, 0, 138, 141, 1, 0, 0, 0, 139, 137, 1, 0, 0, 0, 139, 140, 1, 0, 0, 0, 140, 146, 1, 0, 0, 0, 141, 139, 1, 0, 0, 0, 142, 144, 3, 32, 16, 0, 143, 145, 5, 5, 0, 0, 144, 143, 1, 0, 0, 0, 144, 145, 1, 0, 0, 0, 145, 147, 1, 0, 0, 0, 146, 142, 1, 0, 0, 0, 146, 147, 1, 0, 0, 0, 147, 148, 1, 0, 0, 0, 148, 149, 5, 6, 0, 0, 149, 31, 1, 0, 0, 0, 150, 153, 5, 14, 0, 0, 151, 152, 5, 11, 0, 0, 152, 154, 5, 13, 0, 0, 153, 151, 1, 0, 0, 0, 153, 154, 1, 0, 0, 0, 154, 33, 1, 0, 0, 0, 17, 38, 40, 56, 61, 63, 91, 95, 100, 105, 117, 122, 124, 131, 139, 144, 146, 153] \ No newline at end of file diff --git a/src/gen/bprotoV1.tokens b/src/gen/bprotoV1.tokens new file mode 100644 index 0000000..cf067ce --- /dev/null +++ b/src/gen/bprotoV1.tokens @@ -0,0 +1,29 @@ +T__0=1 +T__1=2 +T__2=3 +T__3=4 +T__4=5 +T__5=6 +T__6=7 +T__7=8 +T__8=9 +T__9=10 +T__10=11 +T__11=12 +INT=13 +IDENTIFIER=14 +WS=15 +COMMENT=16 +COMMENT_MULTILINE=17 +'protocol'=1 +'version'=2 +'message'=3 +'{'=4 +','=5 +'}'=6 +'['=7 +']'=8 +'enum'=9 +'bits'=10 +':'=11 +'='=12 diff --git a/src/gen/bprotoV1Lexer.interp b/src/gen/bprotoV1Lexer.interp new file mode 100644 index 0000000..3c1c232 --- /dev/null +++ b/src/gen/bprotoV1Lexer.interp @@ -0,0 +1,68 @@ +token literal names: +null +'protocol' +'version' +'message' +'{' +',' +'}' +'[' +']' +'enum' +'bits' +':' +'=' +null +null +null +null +null + +token symbolic names: +null +null +null +null +null +null +null +null +null +null +null +null +null +INT +IDENTIFIER +WS +COMMENT +COMMENT_MULTILINE + +rule names: +T__0 +T__1 +T__2 +T__3 +T__4 +T__5 +T__6 +T__7 +T__8 +T__9 +T__10 +T__11 +INT +IDENTIFIER +WS +COMMENT +COMMENT_MULTILINE + +channel names: +DEFAULT_TOKEN_CHANNEL +HIDDEN + +mode names: +DEFAULT_MODE + +atn: +[4, 0, 17, 126, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 4, 12, 86, 8, 12, 11, 12, 12, 12, 87, 1, 13, 4, 13, 91, 8, 13, 11, 13, 12, 13, 92, 1, 14, 4, 14, 96, 8, 14, 11, 14, 12, 14, 97, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 5, 15, 106, 8, 15, 10, 15, 12, 15, 109, 9, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 5, 16, 117, 8, 16, 10, 16, 12, 16, 120, 9, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 118, 0, 17, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 1, 0, 4, 1, 0, 48, 57, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 2, 0, 10, 10, 13, 13, 130, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 1, 35, 1, 0, 0, 0, 3, 44, 1, 0, 0, 0, 5, 52, 1, 0, 0, 0, 7, 60, 1, 0, 0, 0, 9, 62, 1, 0, 0, 0, 11, 64, 1, 0, 0, 0, 13, 66, 1, 0, 0, 0, 15, 68, 1, 0, 0, 0, 17, 70, 1, 0, 0, 0, 19, 75, 1, 0, 0, 0, 21, 80, 1, 0, 0, 0, 23, 82, 1, 0, 0, 0, 25, 85, 1, 0, 0, 0, 27, 90, 1, 0, 0, 0, 29, 95, 1, 0, 0, 0, 31, 101, 1, 0, 0, 0, 33, 112, 1, 0, 0, 0, 35, 36, 5, 112, 0, 0, 36, 37, 5, 114, 0, 0, 37, 38, 5, 111, 0, 0, 38, 39, 5, 116, 0, 0, 39, 40, 5, 111, 0, 0, 40, 41, 5, 99, 0, 0, 41, 42, 5, 111, 0, 0, 42, 43, 5, 108, 0, 0, 43, 2, 1, 0, 0, 0, 44, 45, 5, 118, 0, 0, 45, 46, 5, 101, 0, 0, 46, 47, 5, 114, 0, 0, 47, 48, 5, 115, 0, 0, 48, 49, 5, 105, 0, 0, 49, 50, 5, 111, 0, 0, 50, 51, 5, 110, 0, 0, 51, 4, 1, 0, 0, 0, 52, 53, 5, 109, 0, 0, 53, 54, 5, 101, 0, 0, 54, 55, 5, 115, 0, 0, 55, 56, 5, 115, 0, 0, 56, 57, 5, 97, 0, 0, 57, 58, 5, 103, 0, 0, 58, 59, 5, 101, 0, 0, 59, 6, 1, 0, 0, 0, 60, 61, 5, 123, 0, 0, 61, 8, 1, 0, 0, 0, 62, 63, 5, 44, 0, 0, 63, 10, 1, 0, 0, 0, 64, 65, 5, 125, 0, 0, 65, 12, 1, 0, 0, 0, 66, 67, 5, 91, 0, 0, 67, 14, 1, 0, 0, 0, 68, 69, 5, 93, 0, 0, 69, 16, 1, 0, 0, 0, 70, 71, 5, 101, 0, 0, 71, 72, 5, 110, 0, 0, 72, 73, 5, 117, 0, 0, 73, 74, 5, 109, 0, 0, 74, 18, 1, 0, 0, 0, 75, 76, 5, 98, 0, 0, 76, 77, 5, 105, 0, 0, 77, 78, 5, 116, 0, 0, 78, 79, 5, 115, 0, 0, 79, 20, 1, 0, 0, 0, 80, 81, 5, 58, 0, 0, 81, 22, 1, 0, 0, 0, 82, 83, 5, 61, 0, 0, 83, 24, 1, 0, 0, 0, 84, 86, 7, 0, 0, 0, 85, 84, 1, 0, 0, 0, 86, 87, 1, 0, 0, 0, 87, 85, 1, 0, 0, 0, 87, 88, 1, 0, 0, 0, 88, 26, 1, 0, 0, 0, 89, 91, 7, 1, 0, 0, 90, 89, 1, 0, 0, 0, 91, 92, 1, 0, 0, 0, 92, 90, 1, 0, 0, 0, 92, 93, 1, 0, 0, 0, 93, 28, 1, 0, 0, 0, 94, 96, 7, 2, 0, 0, 95, 94, 1, 0, 0, 0, 96, 97, 1, 0, 0, 0, 97, 95, 1, 0, 0, 0, 97, 98, 1, 0, 0, 0, 98, 99, 1, 0, 0, 0, 99, 100, 6, 14, 0, 0, 100, 30, 1, 0, 0, 0, 101, 102, 5, 47, 0, 0, 102, 103, 5, 47, 0, 0, 103, 107, 1, 0, 0, 0, 104, 106, 8, 3, 0, 0, 105, 104, 1, 0, 0, 0, 106, 109, 1, 0, 0, 0, 107, 105, 1, 0, 0, 0, 107, 108, 1, 0, 0, 0, 108, 110, 1, 0, 0, 0, 109, 107, 1, 0, 0, 0, 110, 111, 6, 15, 0, 0, 111, 32, 1, 0, 0, 0, 112, 113, 5, 47, 0, 0, 113, 114, 5, 42, 0, 0, 114, 118, 1, 0, 0, 0, 115, 117, 9, 0, 0, 0, 116, 115, 1, 0, 0, 0, 117, 120, 1, 0, 0, 0, 118, 119, 1, 0, 0, 0, 118, 116, 1, 0, 0, 0, 119, 121, 1, 0, 0, 0, 120, 118, 1, 0, 0, 0, 121, 122, 5, 42, 0, 0, 122, 123, 5, 47, 0, 0, 123, 124, 1, 0, 0, 0, 124, 125, 6, 16, 0, 0, 125, 34, 1, 0, 0, 0, 6, 0, 87, 92, 97, 107, 118, 1, 6, 0, 0] \ No newline at end of file diff --git a/src/gen/bprotoV1Lexer.py b/src/gen/bprotoV1Lexer.py new file mode 100644 index 0000000..1f2d133 --- /dev/null +++ b/src/gen/bprotoV1Lexer.py @@ -0,0 +1,106 @@ +# Generated from bprotoV1.g4 by ANTLR 4.13.2 +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + + +def serializedATN(): + return [ + 4,0,17,126,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5, + 2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2, + 13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,1,0,1,0,1,0,1,0,1,0,1,0,1, + 0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1, + 2,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,6,1,6,1,7,1,7,1,8,1,8,1,8,1, + 8,1,8,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,12,4,12,86,8,12, + 11,12,12,12,87,1,13,4,13,91,8,13,11,13,12,13,92,1,14,4,14,96,8,14, + 11,14,12,14,97,1,14,1,14,1,15,1,15,1,15,1,15,5,15,106,8,15,10,15, + 12,15,109,9,15,1,15,1,15,1,16,1,16,1,16,1,16,5,16,117,8,16,10,16, + 12,16,120,9,16,1,16,1,16,1,16,1,16,1,16,1,118,0,17,1,1,3,2,5,3,7, + 4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31, + 16,33,17,1,0,4,1,0,48,57,4,0,48,57,65,90,95,95,97,122,3,0,9,10,13, + 13,32,32,2,0,10,10,13,13,130,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0, + 0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0, + 17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0, + 27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,1,35,1,0,0,0,3, + 44,1,0,0,0,5,52,1,0,0,0,7,60,1,0,0,0,9,62,1,0,0,0,11,64,1,0,0,0, + 13,66,1,0,0,0,15,68,1,0,0,0,17,70,1,0,0,0,19,75,1,0,0,0,21,80,1, + 0,0,0,23,82,1,0,0,0,25,85,1,0,0,0,27,90,1,0,0,0,29,95,1,0,0,0,31, + 101,1,0,0,0,33,112,1,0,0,0,35,36,5,112,0,0,36,37,5,114,0,0,37,38, + 5,111,0,0,38,39,5,116,0,0,39,40,5,111,0,0,40,41,5,99,0,0,41,42,5, + 111,0,0,42,43,5,108,0,0,43,2,1,0,0,0,44,45,5,118,0,0,45,46,5,101, + 0,0,46,47,5,114,0,0,47,48,5,115,0,0,48,49,5,105,0,0,49,50,5,111, + 0,0,50,51,5,110,0,0,51,4,1,0,0,0,52,53,5,109,0,0,53,54,5,101,0,0, + 54,55,5,115,0,0,55,56,5,115,0,0,56,57,5,97,0,0,57,58,5,103,0,0,58, + 59,5,101,0,0,59,6,1,0,0,0,60,61,5,123,0,0,61,8,1,0,0,0,62,63,5,44, + 0,0,63,10,1,0,0,0,64,65,5,125,0,0,65,12,1,0,0,0,66,67,5,91,0,0,67, + 14,1,0,0,0,68,69,5,93,0,0,69,16,1,0,0,0,70,71,5,101,0,0,71,72,5, + 110,0,0,72,73,5,117,0,0,73,74,5,109,0,0,74,18,1,0,0,0,75,76,5,98, + 0,0,76,77,5,105,0,0,77,78,5,116,0,0,78,79,5,115,0,0,79,20,1,0,0, + 0,80,81,5,58,0,0,81,22,1,0,0,0,82,83,5,61,0,0,83,24,1,0,0,0,84,86, + 7,0,0,0,85,84,1,0,0,0,86,87,1,0,0,0,87,85,1,0,0,0,87,88,1,0,0,0, + 88,26,1,0,0,0,89,91,7,1,0,0,90,89,1,0,0,0,91,92,1,0,0,0,92,90,1, + 0,0,0,92,93,1,0,0,0,93,28,1,0,0,0,94,96,7,2,0,0,95,94,1,0,0,0,96, + 97,1,0,0,0,97,95,1,0,0,0,97,98,1,0,0,0,98,99,1,0,0,0,99,100,6,14, + 0,0,100,30,1,0,0,0,101,102,5,47,0,0,102,103,5,47,0,0,103,107,1,0, + 0,0,104,106,8,3,0,0,105,104,1,0,0,0,106,109,1,0,0,0,107,105,1,0, + 0,0,107,108,1,0,0,0,108,110,1,0,0,0,109,107,1,0,0,0,110,111,6,15, + 0,0,111,32,1,0,0,0,112,113,5,47,0,0,113,114,5,42,0,0,114,118,1,0, + 0,0,115,117,9,0,0,0,116,115,1,0,0,0,117,120,1,0,0,0,118,119,1,0, + 0,0,118,116,1,0,0,0,119,121,1,0,0,0,120,118,1,0,0,0,121,122,5,42, + 0,0,122,123,5,47,0,0,123,124,1,0,0,0,124,125,6,16,0,0,125,34,1,0, + 0,0,6,0,87,92,97,107,118,1,6,0,0 + ] + +class bprotoV1Lexer(Lexer): + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + T__0 = 1 + T__1 = 2 + T__2 = 3 + T__3 = 4 + T__4 = 5 + T__5 = 6 + T__6 = 7 + T__7 = 8 + T__8 = 9 + T__9 = 10 + T__10 = 11 + T__11 = 12 + INT = 13 + IDENTIFIER = 14 + WS = 15 + COMMENT = 16 + COMMENT_MULTILINE = 17 + + channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ] + + modeNames = [ "DEFAULT_MODE" ] + + literalNames = [ "", + "'protocol'", "'version'", "'message'", "'{'", "','", "'}'", + "'['", "']'", "'enum'", "'bits'", "':'", "'='" ] + + symbolicNames = [ "", + "INT", "IDENTIFIER", "WS", "COMMENT", "COMMENT_MULTILINE" ] + + ruleNames = [ "T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6", + "T__7", "T__8", "T__9", "T__10", "T__11", "INT", "IDENTIFIER", + "WS", "COMMENT", "COMMENT_MULTILINE" ] + + grammarFileName = "bprotoV1.g4" + + def __init__(self, input=None, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.13.2") + self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache()) + self._actions = None + self._predicates = None + + diff --git a/src/gen/bprotoV1Lexer.tokens b/src/gen/bprotoV1Lexer.tokens new file mode 100644 index 0000000..cf067ce --- /dev/null +++ b/src/gen/bprotoV1Lexer.tokens @@ -0,0 +1,29 @@ +T__0=1 +T__1=2 +T__2=3 +T__3=4 +T__4=5 +T__5=6 +T__6=7 +T__7=8 +T__8=9 +T__9=10 +T__10=11 +T__11=12 +INT=13 +IDENTIFIER=14 +WS=15 +COMMENT=16 +COMMENT_MULTILINE=17 +'protocol'=1 +'version'=2 +'message'=3 +'{'=4 +','=5 +'}'=6 +'['=7 +']'=8 +'enum'=9 +'bits'=10 +':'=11 +'='=12 diff --git a/src/gen/bprotoV1Listener.py b/src/gen/bprotoV1Listener.py new file mode 100644 index 0000000..95ff272 --- /dev/null +++ b/src/gen/bprotoV1Listener.py @@ -0,0 +1,165 @@ +# Generated from bprotoV1.g4 by ANTLR 4.13.2 +from antlr4 import * +if "." in __name__: + from .bprotoV1Parser import bprotoV1Parser +else: + from bprotoV1Parser import bprotoV1Parser + +# This class defines a complete listener for a parse tree produced by bprotoV1Parser. +class bprotoV1Listener(ParseTreeListener): + + # Enter a parse tree produced by bprotoV1Parser#protocol_defintion. + def enterProtocol_defintion(self, ctx:bprotoV1Parser.Protocol_defintionContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#protocol_defintion. + def exitProtocol_defintion(self, ctx:bprotoV1Parser.Protocol_defintionContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#protocol_header. + def enterProtocol_header(self, ctx:bprotoV1Parser.Protocol_headerContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#protocol_header. + def exitProtocol_header(self, ctx:bprotoV1Parser.Protocol_headerContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#message_def. + def enterMessage_def(self, ctx:bprotoV1Parser.Message_defContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#message_def. + def exitMessage_def(self, ctx:bprotoV1Parser.Message_defContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#message_id. + def enterMessage_id(self, ctx:bprotoV1Parser.Message_idContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#message_id. + def exitMessage_id(self, ctx:bprotoV1Parser.Message_idContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#enum_def. + def enterEnum_def(self, ctx:bprotoV1Parser.Enum_defContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#enum_def. + def exitEnum_def(self, ctx:bprotoV1Parser.Enum_defContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#bit_field_def. + def enterBit_field_def(self, ctx:bprotoV1Parser.Bit_field_defContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#bit_field_def. + def exitBit_field_def(self, ctx:bprotoV1Parser.Bit_field_defContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#field. + def enterField(self, ctx:bprotoV1Parser.FieldContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#field. + def exitField(self, ctx:bprotoV1Parser.FieldContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#field_pos. + def enterField_pos(self, ctx:bprotoV1Parser.Field_posContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#field_pos. + def exitField_pos(self, ctx:bprotoV1Parser.Field_posContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#dtype. + def enterDtype(self, ctx:bprotoV1Parser.DtypeContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#dtype. + def exitDtype(self, ctx:bprotoV1Parser.DtypeContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#type_standard. + def enterType_standard(self, ctx:bprotoV1Parser.Type_standardContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#type_standard. + def exitType_standard(self, ctx:bprotoV1Parser.Type_standardContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#type_enum. + def enterType_enum(self, ctx:bprotoV1Parser.Type_enumContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#type_enum. + def exitType_enum(self, ctx:bprotoV1Parser.Type_enumContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#type_bitfield. + def enterType_bitfield(self, ctx:bprotoV1Parser.Type_bitfieldContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#type_bitfield. + def exitType_bitfield(self, ctx:bprotoV1Parser.Type_bitfieldContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#array_extension. + def enterArray_extension(self, ctx:bprotoV1Parser.Array_extensionContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#array_extension. + def exitArray_extension(self, ctx:bprotoV1Parser.Array_extensionContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#enum_body. + def enterEnum_body(self, ctx:bprotoV1Parser.Enum_bodyContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#enum_body. + def exitEnum_body(self, ctx:bprotoV1Parser.Enum_bodyContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#enum_field. + def enterEnum_field(self, ctx:bprotoV1Parser.Enum_fieldContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#enum_field. + def exitEnum_field(self, ctx:bprotoV1Parser.Enum_fieldContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#bitfield_body. + def enterBitfield_body(self, ctx:bprotoV1Parser.Bitfield_bodyContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#bitfield_body. + def exitBitfield_body(self, ctx:bprotoV1Parser.Bitfield_bodyContext): + pass + + + # Enter a parse tree produced by bprotoV1Parser#bitfield_field. + def enterBitfield_field(self, ctx:bprotoV1Parser.Bitfield_fieldContext): + pass + + # Exit a parse tree produced by bprotoV1Parser#bitfield_field. + def exitBitfield_field(self, ctx:bprotoV1Parser.Bitfield_fieldContext): + pass + + + +del bprotoV1Parser \ No newline at end of file diff --git a/src/gen/bprotoV1Parser.py b/src/gen/bprotoV1Parser.py new file mode 100644 index 0000000..65bf0d3 --- /dev/null +++ b/src/gen/bprotoV1Parser.py @@ -0,0 +1,1266 @@ +# Generated from bprotoV1.g4 by ANTLR 4.13.2 +# encoding: utf-8 +from antlr4 import * +from io import StringIO +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO + +def serializedATN(): + return [ + 4,1,17,156,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7, + 6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13, + 2,14,7,14,2,15,7,15,2,16,7,16,1,0,1,0,1,0,1,0,4,0,39,8,0,11,0,12, + 0,40,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,5,2,55,8,2, + 10,2,12,2,58,9,2,1,2,1,2,3,2,62,8,2,3,2,64,8,2,1,2,1,2,1,3,1,3,1, + 3,1,3,1,4,1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,7,1, + 7,1,7,1,7,1,8,1,8,1,8,3,8,92,8,8,1,9,1,9,3,9,96,8,9,1,10,1,10,1, + 10,3,10,101,8,10,1,11,1,11,1,11,3,11,106,8,11,1,12,1,12,1,12,1,12, + 1,13,1,13,1,13,1,13,5,13,116,8,13,10,13,12,13,119,9,13,1,13,1,13, + 3,13,123,8,13,3,13,125,8,13,1,13,1,13,1,14,1,14,1,14,3,14,132,8, + 14,1,15,1,15,1,15,1,15,5,15,138,8,15,10,15,12,15,141,9,15,1,15,1, + 15,3,15,145,8,15,3,15,147,8,15,1,15,1,15,1,16,1,16,1,16,3,16,154, + 8,16,1,16,0,0,17,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,0, + 0,157,0,34,1,0,0,0,2,42,1,0,0,0,4,47,1,0,0,0,6,67,1,0,0,0,8,71,1, + 0,0,0,10,75,1,0,0,0,12,79,1,0,0,0,14,84,1,0,0,0,16,91,1,0,0,0,18, + 93,1,0,0,0,20,97,1,0,0,0,22,102,1,0,0,0,24,107,1,0,0,0,26,111,1, + 0,0,0,28,128,1,0,0,0,30,133,1,0,0,0,32,150,1,0,0,0,34,38,3,2,1,0, + 35,39,3,4,2,0,36,39,3,8,4,0,37,39,3,10,5,0,38,35,1,0,0,0,38,36,1, + 0,0,0,38,37,1,0,0,0,39,40,1,0,0,0,40,38,1,0,0,0,40,41,1,0,0,0,41, + 1,1,0,0,0,42,43,5,1,0,0,43,44,5,14,0,0,44,45,5,2,0,0,45,46,5,13, + 0,0,46,3,1,0,0,0,47,48,5,3,0,0,48,49,3,6,3,0,49,50,5,14,0,0,50,56, + 5,4,0,0,51,52,3,12,6,0,52,53,5,5,0,0,53,55,1,0,0,0,54,51,1,0,0,0, + 55,58,1,0,0,0,56,54,1,0,0,0,56,57,1,0,0,0,57,63,1,0,0,0,58,56,1, + 0,0,0,59,61,3,12,6,0,60,62,5,5,0,0,61,60,1,0,0,0,61,62,1,0,0,0,62, + 64,1,0,0,0,63,59,1,0,0,0,63,64,1,0,0,0,64,65,1,0,0,0,65,66,5,6,0, + 0,66,5,1,0,0,0,67,68,5,7,0,0,68,69,5,13,0,0,69,70,5,8,0,0,70,7,1, + 0,0,0,71,72,5,9,0,0,72,73,5,14,0,0,73,74,3,26,13,0,74,9,1,0,0,0, + 75,76,5,10,0,0,76,77,5,14,0,0,77,78,3,30,15,0,78,11,1,0,0,0,79,80, + 3,14,7,0,80,81,5,14,0,0,81,82,5,11,0,0,82,83,3,16,8,0,83,13,1,0, + 0,0,84,85,5,7,0,0,85,86,5,13,0,0,86,87,5,8,0,0,87,15,1,0,0,0,88, + 92,3,18,9,0,89,92,3,20,10,0,90,92,3,22,11,0,91,88,1,0,0,0,91,89, + 1,0,0,0,91,90,1,0,0,0,92,17,1,0,0,0,93,95,5,14,0,0,94,96,3,24,12, + 0,95,94,1,0,0,0,95,96,1,0,0,0,96,19,1,0,0,0,97,100,5,9,0,0,98,101, + 3,26,13,0,99,101,5,14,0,0,100,98,1,0,0,0,100,99,1,0,0,0,101,21,1, + 0,0,0,102,105,5,10,0,0,103,106,3,30,15,0,104,106,5,14,0,0,105,103, + 1,0,0,0,105,104,1,0,0,0,106,23,1,0,0,0,107,108,5,7,0,0,108,109,5, + 13,0,0,109,110,5,8,0,0,110,25,1,0,0,0,111,117,5,4,0,0,112,113,3, + 28,14,0,113,114,5,5,0,0,114,116,1,0,0,0,115,112,1,0,0,0,116,119, + 1,0,0,0,117,115,1,0,0,0,117,118,1,0,0,0,118,124,1,0,0,0,119,117, + 1,0,0,0,120,122,3,28,14,0,121,123,5,5,0,0,122,121,1,0,0,0,122,123, + 1,0,0,0,123,125,1,0,0,0,124,120,1,0,0,0,124,125,1,0,0,0,125,126, + 1,0,0,0,126,127,5,6,0,0,127,27,1,0,0,0,128,131,5,14,0,0,129,130, + 5,12,0,0,130,132,5,13,0,0,131,129,1,0,0,0,131,132,1,0,0,0,132,29, + 1,0,0,0,133,139,5,4,0,0,134,135,3,32,16,0,135,136,5,5,0,0,136,138, + 1,0,0,0,137,134,1,0,0,0,138,141,1,0,0,0,139,137,1,0,0,0,139,140, + 1,0,0,0,140,146,1,0,0,0,141,139,1,0,0,0,142,144,3,32,16,0,143,145, + 5,5,0,0,144,143,1,0,0,0,144,145,1,0,0,0,145,147,1,0,0,0,146,142, + 1,0,0,0,146,147,1,0,0,0,147,148,1,0,0,0,148,149,5,6,0,0,149,31,1, + 0,0,0,150,153,5,14,0,0,151,152,5,11,0,0,152,154,5,13,0,0,153,151, + 1,0,0,0,153,154,1,0,0,0,154,33,1,0,0,0,17,38,40,56,61,63,91,95,100, + 105,117,122,124,131,139,144,146,153 + ] + +class bprotoV1Parser ( Parser ): + + grammarFileName = "bprotoV1.g4" + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + sharedContextCache = PredictionContextCache() + + literalNames = [ "", "'protocol'", "'version'", "'message'", + "'{'", "','", "'}'", "'['", "']'", "'enum'", "'bits'", + "':'", "'='" ] + + symbolicNames = [ "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "INT", "IDENTIFIER", "WS", "COMMENT", + "COMMENT_MULTILINE" ] + + RULE_protocol_defintion = 0 + RULE_protocol_header = 1 + RULE_message_def = 2 + RULE_message_id = 3 + RULE_enum_def = 4 + RULE_bit_field_def = 5 + RULE_field = 6 + RULE_field_pos = 7 + RULE_dtype = 8 + RULE_type_standard = 9 + RULE_type_enum = 10 + RULE_type_bitfield = 11 + RULE_array_extension = 12 + RULE_enum_body = 13 + RULE_enum_field = 14 + RULE_bitfield_body = 15 + RULE_bitfield_field = 16 + + ruleNames = [ "protocol_defintion", "protocol_header", "message_def", + "message_id", "enum_def", "bit_field_def", "field", "field_pos", + "dtype", "type_standard", "type_enum", "type_bitfield", + "array_extension", "enum_body", "enum_field", "bitfield_body", + "bitfield_field" ] + + EOF = Token.EOF + T__0=1 + T__1=2 + T__2=3 + T__3=4 + T__4=5 + T__5=6 + T__6=7 + T__7=8 + T__8=9 + T__9=10 + T__10=11 + T__11=12 + INT=13 + IDENTIFIER=14 + WS=15 + COMMENT=16 + COMMENT_MULTILINE=17 + + def __init__(self, input:TokenStream, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.13.2") + self._interp = ParserATNSimulator(self, self.atn, self.decisionsToDFA, self.sharedContextCache) + self._predicates = None + + + + + class Protocol_defintionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def protocol_header(self): + return self.getTypedRuleContext(bprotoV1Parser.Protocol_headerContext,0) + + + def message_def(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(bprotoV1Parser.Message_defContext) + else: + return self.getTypedRuleContext(bprotoV1Parser.Message_defContext,i) + + + def enum_def(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(bprotoV1Parser.Enum_defContext) + else: + return self.getTypedRuleContext(bprotoV1Parser.Enum_defContext,i) + + + def bit_field_def(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(bprotoV1Parser.Bit_field_defContext) + else: + return self.getTypedRuleContext(bprotoV1Parser.Bit_field_defContext,i) + + + def getRuleIndex(self): + return bprotoV1Parser.RULE_protocol_defintion + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterProtocol_defintion" ): + listener.enterProtocol_defintion(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitProtocol_defintion" ): + listener.exitProtocol_defintion(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitProtocol_defintion" ): + return visitor.visitProtocol_defintion(self) + else: + return visitor.visitChildren(self) + + + + + def protocol_defintion(self): + + localctx = bprotoV1Parser.Protocol_defintionContext(self, self._ctx, self.state) + self.enterRule(localctx, 0, self.RULE_protocol_defintion) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 34 + self.protocol_header() + self.state = 38 + self._errHandler.sync(self) + _la = self._input.LA(1) + while True: + self.state = 38 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [3]: + self.state = 35 + self.message_def() + pass + elif token in [9]: + self.state = 36 + self.enum_def() + pass + elif token in [10]: + self.state = 37 + self.bit_field_def() + pass + else: + raise NoViableAltException(self) + + self.state = 40 + self._errHandler.sync(self) + _la = self._input.LA(1) + if not ((((_la) & ~0x3f) == 0 and ((1 << _la) & 1544) != 0)): + break + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Protocol_headerContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def IDENTIFIER(self): + return self.getToken(bprotoV1Parser.IDENTIFIER, 0) + + def INT(self): + return self.getToken(bprotoV1Parser.INT, 0) + + def getRuleIndex(self): + return bprotoV1Parser.RULE_protocol_header + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterProtocol_header" ): + listener.enterProtocol_header(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitProtocol_header" ): + listener.exitProtocol_header(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitProtocol_header" ): + return visitor.visitProtocol_header(self) + else: + return visitor.visitChildren(self) + + + + + def protocol_header(self): + + localctx = bprotoV1Parser.Protocol_headerContext(self, self._ctx, self.state) + self.enterRule(localctx, 2, self.RULE_protocol_header) + try: + self.enterOuterAlt(localctx, 1) + self.state = 42 + self.match(bprotoV1Parser.T__0) + self.state = 43 + self.match(bprotoV1Parser.IDENTIFIER) + self.state = 44 + self.match(bprotoV1Parser.T__1) + self.state = 45 + self.match(bprotoV1Parser.INT) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Message_defContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def message_id(self): + return self.getTypedRuleContext(bprotoV1Parser.Message_idContext,0) + + + def IDENTIFIER(self): + return self.getToken(bprotoV1Parser.IDENTIFIER, 0) + + def field(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(bprotoV1Parser.FieldContext) + else: + return self.getTypedRuleContext(bprotoV1Parser.FieldContext,i) + + + def getRuleIndex(self): + return bprotoV1Parser.RULE_message_def + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMessage_def" ): + listener.enterMessage_def(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMessage_def" ): + listener.exitMessage_def(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMessage_def" ): + return visitor.visitMessage_def(self) + else: + return visitor.visitChildren(self) + + + + + def message_def(self): + + localctx = bprotoV1Parser.Message_defContext(self, self._ctx, self.state) + self.enterRule(localctx, 4, self.RULE_message_def) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 47 + self.match(bprotoV1Parser.T__2) + self.state = 48 + self.message_id() + self.state = 49 + self.match(bprotoV1Parser.IDENTIFIER) + self.state = 50 + self.match(bprotoV1Parser.T__3) + self.state = 56 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,2,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 51 + self.field() + self.state = 52 + self.match(bprotoV1Parser.T__4) + self.state = 58 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,2,self._ctx) + + self.state = 63 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==7: + self.state = 59 + self.field() + self.state = 61 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==5: + self.state = 60 + self.match(bprotoV1Parser.T__4) + + + + + self.state = 65 + self.match(bprotoV1Parser.T__5) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Message_idContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def INT(self): + return self.getToken(bprotoV1Parser.INT, 0) + + def getRuleIndex(self): + return bprotoV1Parser.RULE_message_id + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMessage_id" ): + listener.enterMessage_id(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMessage_id" ): + listener.exitMessage_id(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMessage_id" ): + return visitor.visitMessage_id(self) + else: + return visitor.visitChildren(self) + + + + + def message_id(self): + + localctx = bprotoV1Parser.Message_idContext(self, self._ctx, self.state) + self.enterRule(localctx, 6, self.RULE_message_id) + try: + self.enterOuterAlt(localctx, 1) + self.state = 67 + self.match(bprotoV1Parser.T__6) + self.state = 68 + self.match(bprotoV1Parser.INT) + self.state = 69 + self.match(bprotoV1Parser.T__7) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Enum_defContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def IDENTIFIER(self): + return self.getToken(bprotoV1Parser.IDENTIFIER, 0) + + def enum_body(self): + return self.getTypedRuleContext(bprotoV1Parser.Enum_bodyContext,0) + + + def getRuleIndex(self): + return bprotoV1Parser.RULE_enum_def + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterEnum_def" ): + listener.enterEnum_def(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitEnum_def" ): + listener.exitEnum_def(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitEnum_def" ): + return visitor.visitEnum_def(self) + else: + return visitor.visitChildren(self) + + + + + def enum_def(self): + + localctx = bprotoV1Parser.Enum_defContext(self, self._ctx, self.state) + self.enterRule(localctx, 8, self.RULE_enum_def) + try: + self.enterOuterAlt(localctx, 1) + self.state = 71 + self.match(bprotoV1Parser.T__8) + self.state = 72 + self.match(bprotoV1Parser.IDENTIFIER) + self.state = 73 + self.enum_body() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Bit_field_defContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def IDENTIFIER(self): + return self.getToken(bprotoV1Parser.IDENTIFIER, 0) + + def bitfield_body(self): + return self.getTypedRuleContext(bprotoV1Parser.Bitfield_bodyContext,0) + + + def getRuleIndex(self): + return bprotoV1Parser.RULE_bit_field_def + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterBit_field_def" ): + listener.enterBit_field_def(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitBit_field_def" ): + listener.exitBit_field_def(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitBit_field_def" ): + return visitor.visitBit_field_def(self) + else: + return visitor.visitChildren(self) + + + + + def bit_field_def(self): + + localctx = bprotoV1Parser.Bit_field_defContext(self, self._ctx, self.state) + self.enterRule(localctx, 10, self.RULE_bit_field_def) + try: + self.enterOuterAlt(localctx, 1) + self.state = 75 + self.match(bprotoV1Parser.T__9) + self.state = 76 + self.match(bprotoV1Parser.IDENTIFIER) + self.state = 77 + self.bitfield_body() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class FieldContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def field_pos(self): + return self.getTypedRuleContext(bprotoV1Parser.Field_posContext,0) + + + def IDENTIFIER(self): + return self.getToken(bprotoV1Parser.IDENTIFIER, 0) + + def dtype(self): + return self.getTypedRuleContext(bprotoV1Parser.DtypeContext,0) + + + def getRuleIndex(self): + return bprotoV1Parser.RULE_field + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterField" ): + listener.enterField(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitField" ): + listener.exitField(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitField" ): + return visitor.visitField(self) + else: + return visitor.visitChildren(self) + + + + + def field(self): + + localctx = bprotoV1Parser.FieldContext(self, self._ctx, self.state) + self.enterRule(localctx, 12, self.RULE_field) + try: + self.enterOuterAlt(localctx, 1) + self.state = 79 + self.field_pos() + self.state = 80 + self.match(bprotoV1Parser.IDENTIFIER) + self.state = 81 + self.match(bprotoV1Parser.T__10) + self.state = 82 + self.dtype() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Field_posContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def INT(self): + return self.getToken(bprotoV1Parser.INT, 0) + + def getRuleIndex(self): + return bprotoV1Parser.RULE_field_pos + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterField_pos" ): + listener.enterField_pos(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitField_pos" ): + listener.exitField_pos(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitField_pos" ): + return visitor.visitField_pos(self) + else: + return visitor.visitChildren(self) + + + + + def field_pos(self): + + localctx = bprotoV1Parser.Field_posContext(self, self._ctx, self.state) + self.enterRule(localctx, 14, self.RULE_field_pos) + try: + self.enterOuterAlt(localctx, 1) + self.state = 84 + self.match(bprotoV1Parser.T__6) + self.state = 85 + self.match(bprotoV1Parser.INT) + self.state = 86 + self.match(bprotoV1Parser.T__7) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class DtypeContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def type_standard(self): + return self.getTypedRuleContext(bprotoV1Parser.Type_standardContext,0) + + + def type_enum(self): + return self.getTypedRuleContext(bprotoV1Parser.Type_enumContext,0) + + + def type_bitfield(self): + return self.getTypedRuleContext(bprotoV1Parser.Type_bitfieldContext,0) + + + def getRuleIndex(self): + return bprotoV1Parser.RULE_dtype + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterDtype" ): + listener.enterDtype(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitDtype" ): + listener.exitDtype(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitDtype" ): + return visitor.visitDtype(self) + else: + return visitor.visitChildren(self) + + + + + def dtype(self): + + localctx = bprotoV1Parser.DtypeContext(self, self._ctx, self.state) + self.enterRule(localctx, 16, self.RULE_dtype) + try: + self.state = 91 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [14]: + self.enterOuterAlt(localctx, 1) + self.state = 88 + self.type_standard() + pass + elif token in [9]: + self.enterOuterAlt(localctx, 2) + self.state = 89 + self.type_enum() + pass + elif token in [10]: + self.enterOuterAlt(localctx, 3) + self.state = 90 + self.type_bitfield() + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Type_standardContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def IDENTIFIER(self): + return self.getToken(bprotoV1Parser.IDENTIFIER, 0) + + def array_extension(self): + return self.getTypedRuleContext(bprotoV1Parser.Array_extensionContext,0) + + + def getRuleIndex(self): + return bprotoV1Parser.RULE_type_standard + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterType_standard" ): + listener.enterType_standard(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitType_standard" ): + listener.exitType_standard(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitType_standard" ): + return visitor.visitType_standard(self) + else: + return visitor.visitChildren(self) + + + + + def type_standard(self): + + localctx = bprotoV1Parser.Type_standardContext(self, self._ctx, self.state) + self.enterRule(localctx, 18, self.RULE_type_standard) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 93 + self.match(bprotoV1Parser.IDENTIFIER) + self.state = 95 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==7: + self.state = 94 + self.array_extension() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Type_enumContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def enum_body(self): + return self.getTypedRuleContext(bprotoV1Parser.Enum_bodyContext,0) + + + def IDENTIFIER(self): + return self.getToken(bprotoV1Parser.IDENTIFIER, 0) + + def getRuleIndex(self): + return bprotoV1Parser.RULE_type_enum + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterType_enum" ): + listener.enterType_enum(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitType_enum" ): + listener.exitType_enum(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitType_enum" ): + return visitor.visitType_enum(self) + else: + return visitor.visitChildren(self) + + + + + def type_enum(self): + + localctx = bprotoV1Parser.Type_enumContext(self, self._ctx, self.state) + self.enterRule(localctx, 20, self.RULE_type_enum) + try: + self.enterOuterAlt(localctx, 1) + self.state = 97 + self.match(bprotoV1Parser.T__8) + self.state = 100 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [4]: + self.state = 98 + self.enum_body() + pass + elif token in [14]: + self.state = 99 + self.match(bprotoV1Parser.IDENTIFIER) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Type_bitfieldContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def bitfield_body(self): + return self.getTypedRuleContext(bprotoV1Parser.Bitfield_bodyContext,0) + + + def IDENTIFIER(self): + return self.getToken(bprotoV1Parser.IDENTIFIER, 0) + + def getRuleIndex(self): + return bprotoV1Parser.RULE_type_bitfield + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterType_bitfield" ): + listener.enterType_bitfield(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitType_bitfield" ): + listener.exitType_bitfield(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitType_bitfield" ): + return visitor.visitType_bitfield(self) + else: + return visitor.visitChildren(self) + + + + + def type_bitfield(self): + + localctx = bprotoV1Parser.Type_bitfieldContext(self, self._ctx, self.state) + self.enterRule(localctx, 22, self.RULE_type_bitfield) + try: + self.enterOuterAlt(localctx, 1) + self.state = 102 + self.match(bprotoV1Parser.T__9) + self.state = 105 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [4]: + self.state = 103 + self.bitfield_body() + pass + elif token in [14]: + self.state = 104 + self.match(bprotoV1Parser.IDENTIFIER) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Array_extensionContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def INT(self): + return self.getToken(bprotoV1Parser.INT, 0) + + def getRuleIndex(self): + return bprotoV1Parser.RULE_array_extension + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterArray_extension" ): + listener.enterArray_extension(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitArray_extension" ): + listener.exitArray_extension(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitArray_extension" ): + return visitor.visitArray_extension(self) + else: + return visitor.visitChildren(self) + + + + + def array_extension(self): + + localctx = bprotoV1Parser.Array_extensionContext(self, self._ctx, self.state) + self.enterRule(localctx, 24, self.RULE_array_extension) + try: + self.enterOuterAlt(localctx, 1) + self.state = 107 + self.match(bprotoV1Parser.T__6) + self.state = 108 + self.match(bprotoV1Parser.INT) + self.state = 109 + self.match(bprotoV1Parser.T__7) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Enum_bodyContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def enum_field(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(bprotoV1Parser.Enum_fieldContext) + else: + return self.getTypedRuleContext(bprotoV1Parser.Enum_fieldContext,i) + + + def getRuleIndex(self): + return bprotoV1Parser.RULE_enum_body + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterEnum_body" ): + listener.enterEnum_body(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitEnum_body" ): + listener.exitEnum_body(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitEnum_body" ): + return visitor.visitEnum_body(self) + else: + return visitor.visitChildren(self) + + + + + def enum_body(self): + + localctx = bprotoV1Parser.Enum_bodyContext(self, self._ctx, self.state) + self.enterRule(localctx, 26, self.RULE_enum_body) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 111 + self.match(bprotoV1Parser.T__3) + self.state = 117 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,9,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 112 + self.enum_field() + self.state = 113 + self.match(bprotoV1Parser.T__4) + self.state = 119 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,9,self._ctx) + + self.state = 124 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==14: + self.state = 120 + self.enum_field() + self.state = 122 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==5: + self.state = 121 + self.match(bprotoV1Parser.T__4) + + + + + self.state = 126 + self.match(bprotoV1Parser.T__5) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Enum_fieldContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def IDENTIFIER(self): + return self.getToken(bprotoV1Parser.IDENTIFIER, 0) + + def INT(self): + return self.getToken(bprotoV1Parser.INT, 0) + + def getRuleIndex(self): + return bprotoV1Parser.RULE_enum_field + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterEnum_field" ): + listener.enterEnum_field(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitEnum_field" ): + listener.exitEnum_field(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitEnum_field" ): + return visitor.visitEnum_field(self) + else: + return visitor.visitChildren(self) + + + + + def enum_field(self): + + localctx = bprotoV1Parser.Enum_fieldContext(self, self._ctx, self.state) + self.enterRule(localctx, 28, self.RULE_enum_field) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 128 + self.match(bprotoV1Parser.IDENTIFIER) + self.state = 131 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==12: + self.state = 129 + self.match(bprotoV1Parser.T__11) + self.state = 130 + self.match(bprotoV1Parser.INT) + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Bitfield_bodyContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def bitfield_field(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(bprotoV1Parser.Bitfield_fieldContext) + else: + return self.getTypedRuleContext(bprotoV1Parser.Bitfield_fieldContext,i) + + + def getRuleIndex(self): + return bprotoV1Parser.RULE_bitfield_body + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterBitfield_body" ): + listener.enterBitfield_body(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitBitfield_body" ): + listener.exitBitfield_body(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitBitfield_body" ): + return visitor.visitBitfield_body(self) + else: + return visitor.visitChildren(self) + + + + + def bitfield_body(self): + + localctx = bprotoV1Parser.Bitfield_bodyContext(self, self._ctx, self.state) + self.enterRule(localctx, 30, self.RULE_bitfield_body) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 133 + self.match(bprotoV1Parser.T__3) + self.state = 139 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,13,self._ctx) + while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: + if _alt==1: + self.state = 134 + self.bitfield_field() + self.state = 135 + self.match(bprotoV1Parser.T__4) + self.state = 141 + self._errHandler.sync(self) + _alt = self._interp.adaptivePredict(self._input,13,self._ctx) + + self.state = 146 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==14: + self.state = 142 + self.bitfield_field() + self.state = 144 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==5: + self.state = 143 + self.match(bprotoV1Parser.T__4) + + + + + self.state = 148 + self.match(bprotoV1Parser.T__5) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class Bitfield_fieldContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def IDENTIFIER(self): + return self.getToken(bprotoV1Parser.IDENTIFIER, 0) + + def INT(self): + return self.getToken(bprotoV1Parser.INT, 0) + + def getRuleIndex(self): + return bprotoV1Parser.RULE_bitfield_field + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterBitfield_field" ): + listener.enterBitfield_field(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitBitfield_field" ): + listener.exitBitfield_field(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitBitfield_field" ): + return visitor.visitBitfield_field(self) + else: + return visitor.visitChildren(self) + + + + + def bitfield_field(self): + + localctx = bprotoV1Parser.Bitfield_fieldContext(self, self._ctx, self.state) + self.enterRule(localctx, 32, self.RULE_bitfield_field) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 150 + self.match(bprotoV1Parser.IDENTIFIER) + self.state = 153 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==11: + self.state = 151 + self.match(bprotoV1Parser.T__10) + self.state = 152 + self.match(bprotoV1Parser.INT) + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + + + diff --git a/src/gen/bprotoV1Visitor.py b/src/gen/bprotoV1Visitor.py new file mode 100644 index 0000000..b56537a --- /dev/null +++ b/src/gen/bprotoV1Visitor.py @@ -0,0 +1,98 @@ +# Generated from bprotoV1.g4 by ANTLR 4.13.2 +from antlr4 import * +if "." in __name__: + from .bprotoV1Parser import bprotoV1Parser +else: + from bprotoV1Parser import bprotoV1Parser + +# This class defines a complete generic visitor for a parse tree produced by bprotoV1Parser. + +class bprotoV1Visitor(ParseTreeVisitor): + + # Visit a parse tree produced by bprotoV1Parser#protocol_defintion. + def visitProtocol_defintion(self, ctx:bprotoV1Parser.Protocol_defintionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#protocol_header. + def visitProtocol_header(self, ctx:bprotoV1Parser.Protocol_headerContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#message_def. + def visitMessage_def(self, ctx:bprotoV1Parser.Message_defContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#message_id. + def visitMessage_id(self, ctx:bprotoV1Parser.Message_idContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#enum_def. + def visitEnum_def(self, ctx:bprotoV1Parser.Enum_defContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#bit_field_def. + def visitBit_field_def(self, ctx:bprotoV1Parser.Bit_field_defContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#field. + def visitField(self, ctx:bprotoV1Parser.FieldContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#field_pos. + def visitField_pos(self, ctx:bprotoV1Parser.Field_posContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#dtype. + def visitDtype(self, ctx:bprotoV1Parser.DtypeContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#type_standard. + def visitType_standard(self, ctx:bprotoV1Parser.Type_standardContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#type_enum. + def visitType_enum(self, ctx:bprotoV1Parser.Type_enumContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#type_bitfield. + def visitType_bitfield(self, ctx:bprotoV1Parser.Type_bitfieldContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#array_extension. + def visitArray_extension(self, ctx:bprotoV1Parser.Array_extensionContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#enum_body. + def visitEnum_body(self, ctx:bprotoV1Parser.Enum_bodyContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#enum_field. + def visitEnum_field(self, ctx:bprotoV1Parser.Enum_fieldContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#bitfield_body. + def visitBitfield_body(self, ctx:bprotoV1Parser.Bitfield_bodyContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by bprotoV1Parser#bitfield_field. + def visitBitfield_field(self, ctx:bprotoV1Parser.Bitfield_fieldContext): + return self.visitChildren(ctx) + + + +del bprotoV1Parser \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..0165311 --- /dev/null +++ b/src/main.py @@ -0,0 +1,62 @@ +import argparse + +from backend.rendering.pythonBackend import PythonBackendRenderer +from backend.rendering.cBackend import CBackendRenderer +from backend.rendering.txtBackend import TxtBackendRenderer +from compiler import BprotoCompiler + +INPUT_FILE = "examples/test_protocol.bproto" + + +def main(): + cli_args_parser = argparse.ArgumentParser(description="Bproto Compiler") + + cli_args_parser.add_argument( + "-i", "--input", + help="Input file to compile", + required=True + ) + cli_args_parser.add_argument( + "-o", "--output", + help="Output file to write compiled code to", + required=True + ) + cli_args_parser.add_argument( + "-t", "--target", + help="Target language to compile to", + required=True, + choices=["python", "c", "txt"], + action="append" + ) + + args = cli_args_parser.parse_args() + + with open(args.input, "r") as f: + source_text = f.read() + + # Backend configuration + backends = [] + + if "python" in args.target: + print("Compiling to Python") + backends.append(PythonBackendRenderer("python")) + + if "c" in args.target: + print("Compiling to C") + backends.append(CBackendRenderer("c")) + + if "txt" in args.target: + print("Compiling to Txt") + backends.append(TxtBackendRenderer("txt")) + + compiler = BprotoCompiler(backends, "template") + output = compiler.compile(source_text, ".") + + output.saveToDisk(args.output) + string_outputs = output.toString() + print("Output Files:", list(string_outputs.keys())) + print(string_outputs.get("txt/protocolSummary.txt")) + + +if __name__ == '__main__': + main() diff --git a/src/nameHandling/base.py b/src/nameHandling/base.py new file mode 100644 index 0000000..109c33a --- /dev/null +++ b/src/nameHandling/base.py @@ -0,0 +1,169 @@ +from typing import Literal +from abc import ABC, abstractmethod + +import re +from copy import deepcopy + + +class ComponentName: + """Class to represent a component name. A portable container for component names. + Should be used instead of strings to represent component like bitfields, enums, messages, etc. + + Conversions to diffrent target styles are implemented separately via ComponentNameStyle classes. + """ + + def __init__(self, name_parts: list = [], source_style: object | None = None, source_string: str = ""): + """Initialize a ComponentName object. + + Args: + name_parts (list, optional): list of lowercase name components as string . Defaults to []. + source_style (object | None, optional): ComponentNameStyle use to create this ComponentName. This is used for compare other strings agains. Defaults to None. + source_string (str, optional): String representation of the name. Defaults to "". This is used for clearification in error messages. + """ + self.name_parts: list[str] = name_parts + self.source_style = source_style + self.source_string = source_string + + def __repr__(self): + return f"ComponentName({'-'.join(map(lambda s: s[0].upper() + s[1:].lower(), self.name_parts))})" + + def __eq__(self, value: object | str) -> bool: + if isinstance(value, ComponentName): + return self.name_parts == value.name_parts + elif isinstance(value, str) and self.source_style is not None: + return self.source_style.fromStr(value) == self + else: + return False + + def __add__(self, other: str): + if isinstance(other, type(self)): + return ComponentName( + self.name_parts + other.name_parts, + self.source_style + ) + + elif isinstance(other, str): + return ComponentName( + self.name_parts + self.source_style.fromStr(other).name_parts, + self.source_style + ) + + else: + raise TypeError(f"Cannot combinde {type(other)} with ComponentName") + + def __hash__(self): + return hash(tuple(self.name_parts)) + + def __deepcopy__(self, memo={}): + if id(self) in memo: + return memo[id(self)] + + res = ComponentName( + [], + self.source_style, + self.source_string + ) + + res.name_parts = deepcopy(self.name_parts, memo) + res.source_style = deepcopy(self.source_style, memo) + res.source_string = deepcopy(self.source_string, memo) + + memo[id(self)] = res + memo[id(res)] = res + return res + + +class NameStyle(ABC): + """Abstract class to represent a style of component names. + This converts ComponentNames to strings and vice versa. + """ + + def __init__(self): + pass + + @staticmethod + @abstractmethod + def toStr(component_name: ComponentName, context: str = "") -> str: + """Given a ComponentName object, convert it to a string in the target style. + + Args: + component_name (NameStyle): The ComponentName object to convert. + context (str): Additional context to use when converting the name. + + Returns: + str: String representation of the ComponentName object in a target style. + """ + pass + + @staticmethod + @abstractmethod + def fromStr(name: str, context: str = "") -> ComponentName: + """Given a string in the target style, convert it to a ComponentName object. + + Args: + name (str): The string to convert. + context (str): Additional context to use when converting the name. + + Returns: + ComponentName: ComponentName object representing a name + """ + pass + + +class NameStyleBproto(NameStyle): + """Converter class for bproto style component names. + + Inherited from NameStyle. + """ + + @staticmethod + def toStr(component_name: ComponentName, context: str = Literal["", "enum", "member"]) -> str: + + if context == "enum": + return '_'.join(map(lambda s: s.upper(), component_name.name_parts)) + elif context == "member": + res = ''.join(map(lambda s: s[0].upper() + s[1:].lower(), component_name.name_parts)) + return res[0].lower() + res[1:] + else: + return ''.join(map(lambda s: s[0].upper() + s[1:].lower(), component_name.name_parts)) + + @staticmethod + def fromStr(bproto_style_name: str | ComponentName, context: Literal["", "enum", "protocol_name"] = "") -> ComponentName: + if isinstance(bproto_style_name, ComponentName): + bproto_style_name.source_style = NameStyleBproto + return bproto_style_name + + first_split = re.split(r"-|_", bproto_style_name) + second_split: list[str] = [] + + if context == "enum" or context == "protocol_name": + second_split.extend(list(map(lambda s: s.lower(), first_split))) + else: + for part in first_split: + second_split.extend( + list(map(lambda s: s.lower(), + re.split(r'(?=[A-Z])', part) + )) + ) + + while "" in second_split: + second_split.remove("") + + return ComponentName(second_split, NameStyleBproto, bproto_style_name) + + +def to_cap_case(component_name: ComponentName) -> str: + return "".join([i[0].upper() + i[1:].lower() for i in component_name.name_parts]) + + +def to_camel_case(component_name: ComponentName) -> str: + res = "".join([i[0].upper() + i[1:].lower() for i in component_name.name_parts]) + return res[0].lower() + res[1:] + + +def to_snake_case(component_name: ComponentName) -> str: + return "_".join([i.lower() for i in component_name.name_parts]) + + +def to_screaming_case(component_name: ComponentName) -> str: + return "_".join([i.upper() for i in component_name.name_parts]) diff --git a/src/nameHandling/brand_applier.py b/src/nameHandling/brand_applier.py new file mode 100644 index 0000000..d6b478c --- /dev/null +++ b/src/nameHandling/brand_applier.py @@ -0,0 +1,53 @@ +from abc import ABC, abstractmethod +from nameHandling.base import ComponentName, NameStyleBproto +from protocol_components.message import Message + + +class ComponentNameModifier(ABC): + """A generic statefull 'maschine' that modifies (apply) a change to a ComponentName. + + This is an abstract class + """ + @abstractmethod + def apply(self, name: ComponentName) -> ComponentName: + """Apply the change to the ComponentName. + + This changes the component name in place and returns it. + + Args: + name (ComponentName): The ComponentName to modify. + + Returns: + ComponentName: Modified ComponentName + """ + pass + + +class BrandApplier(ComponentNameModifier): + """Applies a protocol brand name to a ComponentName. + Just prepends the brand name to the ComponentName. + + Inherited from ComponentNameModifier. + """ + def __init__(self, brand_name: str | ComponentName): + """Initialize the BrandApplier. + + Args: + brand_name (str|ComponentName): Brand name to apply. + """ + if isinstance(brand_name, ComponentName): + self.brand_name: ComponentName = brand_name + elif isinstance(brand_name, str): + self.brand_name: ComponentName = NameStyleBproto.fromStr(brand_name) + else: + raise TypeError("Invalid Type for Brand Name") + + def apply(self, obj: ComponentName | Message) -> ComponentName: + + if isinstance(obj, ComponentName): + return self.brand_name + obj + elif isinstance(obj, Message): + obj.name = self.apply(obj.name) + return obj + else: + raise TypeError(f"Cannot apply naming to type {type(obj)}") diff --git a/src/nameHandling/resolver.py b/src/nameHandling/resolver.py new file mode 100644 index 0000000..78b1c59 --- /dev/null +++ b/src/nameHandling/resolver.py @@ -0,0 +1,95 @@ +from protocol_components.field import Field, FieldEnum, FieldBitfield, FieldEnumRef, FieldBitfieldRef, FactoryField +from protocol_components.dtypes import BprotoFieldBaseType +from protocol_components.message import Message +from nameHandling.brand_applier import BrandApplier +import protocol_components.protocol + +from errors import BprotoUnresolvedReferenceError + + +class ProtocolReferenceResolver: + """Resolves refences in fields in messages in protocol definitiown + This is a one-time use class, once the resolve_refrence_protocol is called, the object should not be used anymore + + Usally used after the ProtocolDefinitions object is created by the FactoryProtocolDefition + """ + + def __init__(self, protocol: protocol_components.protocol.ProtocolDefinitions): + """Initializes the ProtocolReferenceResolver class + + Args: + protocol (ProtocolDefinitions): Protocol to resolve refrences in, must not be fully assembled by the ProtocolFactory + """ + self.protocol = protocol + + def resolve_refrence_protocol(self) -> protocol_components.protocol.ProtocolDefinitions: + """Resolves refrences in the protocol definitiown + This modifies the protocol definitiown in place and returns it + + Returns: + ProtocolDefinitions: Protocol definitiown with refrences resolved + """ + for name, message in self.protocol.messages.items(): + self.protocol.messages[name] = self._resolve_refrence_fields_in_message(message) + return self.protocol + + def _resolve_refrence_fields_in_message(self, message: Message) -> Message: + for name, field in message.fields.items(): + message.fields[name] = self._resolve_refrence_fields(field, message) + return message + + def _resolve_refrence_fields(self, field: Field, parent_message: Message) -> Field: + if isinstance(field, FieldBitfieldRef): + return self._resolve_refrence_bitfield(field, parent_message) + elif isinstance(field, FieldEnumRef): + return self._resolve_refrence_enum(field, parent_message) + else: + return field + + def _resolve_refrence_bitfield(self, field: FieldBitfieldRef, parent_message: Message) -> FieldBitfield: + if field.ref not in self.protocol.bitfields: + raise BprotoUnresolvedReferenceError(field.ref, field, parent_message) + + factory = FactoryField() + return factory.assemble( + field.name, + field.pos, + BprotoFieldBaseType.BITFIELD, + field.array_size, + self.protocol.bitfields[field.ref] + ) + + def _resolve_refrence_enum(self, field: FieldEnumRef, parent_message: Message) -> FieldEnum: + if field.ref not in self.protocol.enums: + raise BprotoUnresolvedReferenceError(field.ref, field, parent_message) + + factory = FactoryField() + return factory.assemble( + field.name, + field.pos, + BprotoFieldBaseType.ENUM, + field.array_size, + self.protocol.enums[field.ref] + ) + + def back_resolve_inlines(self) -> protocol_components.protocol.ProtocolDefinitions: + inline_brand_applyer = BrandApplier("inline") + for msg_name, msg in self.protocol.messages.items(): + for field_name, field in msg.fields.items(): + if isinstance(field, FieldBitfield) and field.bitfield.inline: + field.bitfield.name = inline_brand_applyer.apply( + msg.name + field.name + ) + self.protocol.bitfields.update({ + field.bitfield.name: field.bitfield + }) + + elif isinstance(field, FieldEnum) and field.enum.inline: + field.enum.name = inline_brand_applyer.apply( + msg.name + field.name + ) + self.protocol.enums.update({ + field.enum.name: field.enum + }) + + return self.protocol diff --git a/src/nameHandling/style/cNameStyle.py b/src/nameHandling/style/cNameStyle.py new file mode 100644 index 0000000..5c1b43d --- /dev/null +++ b/src/nameHandling/style/cNameStyle.py @@ -0,0 +1,75 @@ +from nameHandling.base import ComponentName, NameStyle, to_cap_case, to_camel_case, to_screaming_case +from protocol_components.protocol import ProtocolDefinitions + +import re +from typing import Literal + + +class NameStyleC(NameStyle): + + @staticmethod + def toStr(bproto_style_name: str | ComponentName, context: + Literal[ + "", "enum_name", "enum_item", + "struct_name", "struct_member", + "bitfield_name", "bitfield_member", + ] = "") -> str: + + match context: + case "struct_name": + res = to_cap_case(bproto_style_name) + case "struct_member": + res = to_camel_case(bproto_style_name) + case "enum_name": + res = to_cap_case(bproto_style_name) + case "enum_item": + res = to_screaming_case(bproto_style_name) + case "bitfield_name": + res = to_cap_case(bproto_style_name) + case "bitfield_member": + res = to_camel_case(bproto_style_name) + case _: + res = to_camel_case(bproto_style_name) + + res = re.sub(r"_+", "_", res) + + return res + + @staticmethod + def fromStr(bproto_style_name: str | ComponentName, context: str = "") -> ComponentName: + raise NotImplementedError("This mode is not avaialbe for NameStyleC") + + @staticmethod + def preprocess(protocol: ProtocolDefinitions) -> ProtocolDefinitions: + # Enum Mod + protocol.enums = { + protocol.name + ComponentName(["_enum_"]) + v.name: v + for _, v in protocol.enums.items() + } + + for enum_name, e in protocol.enums.items(): + e.name = enum_name + e.values = { + enum_name + "value" + k: v + for k, v in e.values.items() + } + + # Bitfield mod + protocol.bitfields = { + protocol.name + ComponentName(["_bitfield_"]) + v.name: v + for _, v in protocol.bitfields.items() + } + + for name, b in protocol.bitfields.items(): + b.name = name + + # Message mod + protocol.messages = { + protocol.name + ComponentName(["_message_"]) + v.name: v + for _, v in protocol.messages.items() + } + + for name, m in protocol.messages.items(): + m.name = name + + return protocol diff --git a/src/nameHandling/style/pythonNameStyle.py b/src/nameHandling/style/pythonNameStyle.py new file mode 100644 index 0000000..afc788d --- /dev/null +++ b/src/nameHandling/style/pythonNameStyle.py @@ -0,0 +1,57 @@ +from typing import Literal +from nameHandling.base import ComponentName, NameStyle, to_cap_case, to_snake_case, to_screaming_case +from protocol_components.protocol import ProtocolDefinitions + + +class NameStylePython(NameStyle): + + @staticmethod + def toStr(bproto_style_name: str | ComponentName, context: + Literal[ + "", "enum_name", "enum_item", + "class_name", "class_member", + ] = "") -> str: + + match context: + case "class_name": + return to_cap_case(bproto_style_name) + case "class_member": + return to_snake_case(bproto_style_name) + case "enum_name": + return to_cap_case(bproto_style_name) + case "enum_item": + return to_screaming_case(bproto_style_name) + case _: + return to_snake_case(bproto_style_name) + + @staticmethod + def fromStr(bproto_style_name: str | ComponentName, context: str = "") -> ComponentName: + raise NotImplementedError("This mode is not avaialbe for NameStylePython") + + @staticmethod + def preprocess(protocol: ProtocolDefinitions) -> ProtocolDefinitions: + protocol.enums = { + protocol.name + "Enum" + v.name: v + for _, v in protocol.enums.items() + } + + for name, e in protocol.enums.items(): + e.name = name + + protocol.bitfields = { + protocol.name + "Bitfield" + v.name: v + for _, v in protocol.bitfields.items() + } + + for name, b in protocol.bitfields.items(): + b.name = name + + protocol.messages = { + protocol.name + "Message" + v.name: v + for _, v in protocol.messages.items() + } + + for name, m in protocol.messages.items(): + m.name = name + + return protocol diff --git a/src/parser/ast_visitor.py b/src/parser/ast_visitor.py new file mode 100644 index 0000000..d055d5a --- /dev/null +++ b/src/parser/ast_visitor.py @@ -0,0 +1,157 @@ +from protocol_components import AbstractProtocolComponent +from protocol_components.dtypes import BprotoFieldBaseType +from protocol_components.field import Field, FactoryField +from protocol_components.enumeration import FactoryEnumeration +from protocol_components.bitfields import FactoryBitfield +from protocol_components.message import Message, FactoryMessage +from protocol_components.protocol import ProtocolDefinitions +from protocol_components.protocolFactory import FactoryProtocolDefition + +from gen.bprotoV1Visitor import bprotoV1Visitor as BprotoVisitor +from antlr4 import TerminalNode + + +class BprotoASTVisitor(BprotoVisitor): + # Defintion parsing + def visitProtocol_defintion(self, ctx) -> ProtocolDefinitions: + factory = FactoryProtocolDefition() + + name, version = self.visit(ctx.protocol_header()) + + for message in ctx.message_def(): + message_object = self.visit(message) + factory.add_message(message_object) + + for enum in ctx.enum_def(): + enum_object = self.visit(enum) + factory.add_enum(enum_object) + + for bitfield in ctx.bit_field_def(): + bitfield_object = self.visit(bitfield) + factory.add_bitfield(bitfield_object) + + return factory.assemble(name, version) + + def visitProtocol_header(self, ctx): + return ctx.IDENTIFIER().getText(), int(ctx.INT().getText()) + + def visitBit_field_def(self, ctx): + factory: FactoryBitfield = self.visit(ctx.bitfield_body()) + name = ctx.IDENTIFIER().getText() + return factory.assemble(name) + + def visitEnum_def(self, ctx): + factory: FactoryEnumeration = self.visit(ctx.enum_body()) + name = ctx.IDENTIFIER().getText() + return factory.assemble(name) + + # Message definition parsing + def visitMessage_def(self, ctx) -> Message: + identifier = ctx.IDENTIFIER().getText() + index_number = self.visit(ctx.message_id()) + + factory = FactoryMessage() + + fields = ctx.field() + + for field in fields: + factory.add_field(self.visit(field)) + + return factory.assemble(identifier, index_number) + + def visitMessage_id(self, ctx) -> int: + return int(ctx.INT().getText()) + + def visitField(self, ctx) -> Field: + identfier = ctx.IDENTIFIER().getText() + pos = self.visit(ctx.field_pos()) + field_type, array_size, refrence = self.visit(ctx.dtype()) + + factory = FactoryField() + + return factory.assemble( + identfier, + pos, + field_type, + array_size, + refrence + ) + + def visitField_pos(self, ctx) -> int: + return int(ctx.INT().getText()) + + # Field Type parsing + def visitDtype(self, ctx) -> tuple[BprotoFieldBaseType, int, None | str | AbstractProtocolComponent]: + if ctx.type_enum(): + return self.visit(ctx.type_enum()) + elif ctx.type_bitfield(): + return self.visit(ctx.type_bitfield()) + elif ctx.type_standard(): + return self.visit(ctx.type_standard()) + + def visitType_standard(self, ctx) -> tuple[BprotoFieldBaseType, int, None]: + return ( + ctx.IDENTIFIER().getText(), + self.visit(ctx.array_extension()) + if ctx.array_extension() else 1, + None, + ) + + def visitArray_extension(self, ctx): + return int(ctx.INT().getText()) + + def visitType_enum(self, ctx) -> tuple[BprotoFieldBaseType, int, AbstractProtocolComponent | str]: + if isinstance(ctx.getChild(1), TerminalNode): + ref_name = ctx.IDENTIFIER().getText() + return BprotoFieldBaseType.ENUM, 1, ref_name + else: + enum_factory: FactoryEnumeration = self.visit(ctx.enum_body()) + return BprotoFieldBaseType.ENUM, 1, enum_factory.assemble("", inline=True) + + def visitType_bitfield(self, ctx): + if isinstance(ctx.getChild(1), TerminalNode): + ref_name = ctx.IDENTIFIER().getText() + return BprotoFieldBaseType.BITFIELD, 1, ref_name + else: + factory: FactoryBitfield = self.visit(ctx.bitfield_body()) + return BprotoFieldBaseType.BITFIELD, 1, factory.assemble("", inline=True) + + # Enum parsing + def visitEnum_body(self, ctx) -> FactoryEnumeration: + enumeration_factory = FactoryEnumeration() + + for field in ctx.enum_field(): + name, value = self.visit(field) + enumeration_factory.add_value(name, value) + + return enumeration_factory + + def visitEnum_field(self, ctx): + name = ctx.IDENTIFIER().getText() + + if ctx.getChildCount() > 1: + value = int(ctx.INT().getText()) + else: + value = None + + return name, value + + # Bit Field parsing + def visitBitfield_body(self, ctx): + factory = FactoryBitfield() + + for field in ctx.bitfield_field(): + name, pos = self.visit(field) + factory.add_bit(name, pos) + + return factory + + def visitBitfield_field(self, ctx): + name = ctx.IDENTIFIER().getText() + + if ctx.getChildCount() > 1: + position = int(ctx.INT().getText()) + else: + position = None + + return name, position diff --git a/src/parser/parser.py b/src/parser/parser.py new file mode 100644 index 0000000..4bcde5d --- /dev/null +++ b/src/parser/parser.py @@ -0,0 +1,120 @@ +from antlr4 import InputStream, CommonTokenStream, ParserRuleContext, DFA +from antlr4.error.ErrorListener import ErrorListener + +from gen.bprotoV1Lexer import bprotoV1Lexer as BprotoLexer +from gen.bprotoV1Parser import bprotoV1Parser as BprotoParser + + +class bproto_ErrorListener(ErrorListener): + """ + Error listener for the ANTLR parser. + This handles/collects syntax errors and formats them for the user + """ + + def __init__(self): + super().__init__() + self.syntax_error_list: list[tuple[int, int, str, Exception]] = [] + self.previous_line = 5 + + def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): + self.syntax_error_list.append((line, column, msg, e)) + + def reportAmbiguity(self, recognizer, dfa: DFA, startIndex, + stopIndex, exact, ambigAlts, configs): + pass + + def reportAttemptingFullContext(self, recognizer, dfa, startIndex, + stopIndex, conflictingAlts, configs): + pass + + def reportContextSensitivity(self, recognizer, dfa, startIndex, + stopIndex, prediction, configs): + pass + + @staticmethod + def make_code_preview( + source_text: list[str], line: int, column: int) -> str: + + code_window = "" + for i in range(max(0, line - 5), line): + code_window += f"{i + 1:3d} | {source_text[i]}\n" + code_window += " | " + " " * (column) + "^\n" + return code_window + + def get_syntax_error_message(self, source_text: str) -> str: + """generates a humman readable error message from the syntax errors + + Args: + source_text (str): The source text that was parsed + + Returns: + str: The human readable error message (multi-line) + """ + source_text: list[str] = source_text.split("\n") + result_error_message = "" + + for line, column, msg, e in self.syntax_error_list: + result_error_message += bproto_ErrorListener.make_code_preview( + source_text, line, column + ) + result_error_message += f"line {line} column {column}: {msg}\n\n" + + result_error_message += f"Found {len(self.syntax_error_list)} " \ + + "syntax errors" + + return result_error_message + + +def create_ast_parser(protocol_defintion_text: str, error_message_listner: bproto_ErrorListener) -> BprotoParser: + """Creates an ANTLR parser for the given protocol definition text + This function just creates and configures the parser, it does not parse the text + + Args: + protocol_defintion_text (str): The protocol definition text from the source files + error_message_listner (bproto_ErrorListener): The error listener to collect syntax errors + + Returns: + BprotoParser: The configured ANTLR parser, ready to walk the tree + """ + input_stream = InputStream(protocol_defintion_text) + + antlr_lexer = BprotoLexer(input_stream) + antlr_lexer.removeErrorListeners() + antlr_lexer.addErrorListener(error_message_listner) + + antlr_token_stream = CommonTokenStream(antlr_lexer) + + antlr_parser = BprotoParser(antlr_token_stream) + antlr_parser.removeErrorListeners() + antlr_parser.addErrorListener(error_message_listner) + + return antlr_parser + + +def parse_ast_string( + protocol_defintion_text: str +) -> ParserRuleContext | bproto_ErrorListener: + """ + Parse the protocol definition text into an AST. + If there are syntax errors, return an error listener with the syntax errors + + Args: + protocol_defintion_text (str): The protocol definition text from the + source files + Returns: + ParserRuleContext | bproto_ErrorListener: The parsed AST if successful, + otherwise an error listener with syntax errors for reporting + """ + error_message_listner = bproto_ErrorListener() + + antlr_parser = create_ast_parser( + protocol_defintion_text, + error_message_listner + ) + + ast = antlr_parser.protocol_defintion() + + if antlr_parser.getNumberOfSyntaxErrors() > 0: + return error_message_listner + + return ast diff --git a/src/protocol_components/__init__.py b/src/protocol_components/__init__.py new file mode 100644 index 0000000..674bf29 --- /dev/null +++ b/src/protocol_components/__init__.py @@ -0,0 +1,91 @@ +from abc import ABC, abstractmethod +from collections import OrderedDict +from nameHandling.base import ComponentName + + +class AbstractProtocolComponent(ABC): + @abstractmethod + def get_identifier(self) -> int | ComponentName: + """Get the identifier of the protocol component. + + This identifier should be unique and is ment for sorting and indexing. + + Returns: + int | str: Identifier of the protocol component + """ + pass + + @abstractmethod + def get_name(self) -> ComponentName: + """Get the display name of the protocol component. + + This is ment to display error messages, render into code, etc. + Should be human readable and unique. + + Returns: + str: Display name of the protocol component + """ + pass + + @abstractmethod + def get_type_name(self) -> str: + """Get the type name of the protocol component. + + This is ment to display error messages. + Should be human readable and describe the type of the component. + + Returns: + str: Type name of the protocol component + """ + pass + + @abstractmethod + def get_size_bytes(self) -> int: + """Get the size of the protocol component in bytes. + Should be the same as ceil(get_bitsize() / 8). + + Returns: + int: Size of the protocol component in bytes + """ + pass + + @abstractmethod + def get_size_bits(self) -> int: + """Get the size of the protocol component in bits. + Should be the same as get_byte_size() * 8. + + Returns: + int: Size of the protocol component in bits + """ + pass + + +def sort_dict_by_value(d: dict[str, int]) -> dict[str, int]: + """Sort a dictionary by its values. + Values should be something sortable. + + Args: + d (dict[str, int]): dictionary to be sorted + + Returns: + dict[str,int]: Sorted dictionary + """ + return OrderedDict(sorted(d.items(), key=lambda x: x[1])) + + +def find_smallset_next_value(s: set[int], inital=0) -> int: + """Find the next smallest value that is not in the set. + Used to fill in missing values in an enumeration or bitfield. + + Args: + s (set[int]): Set of values + inital (int, optional): Start value to search from. Defaults to 0. For optimization purposes. + + Returns: + int: Next smallest value not in the set + """ + + i = inital + while i in s: + i += 1 + return i diff --git a/src/protocol_components/bitfields.py b/src/protocol_components/bitfields.py new file mode 100644 index 0000000..5326bc7 --- /dev/null +++ b/src/protocol_components/bitfields.py @@ -0,0 +1,167 @@ +from . import AbstractProtocolComponent, find_smallset_next_value, sort_dict_by_value +from errors import BprotoDuplicateNameError, BprotoDuplicateBitfieldPositionError +from nameHandling.base import ComponentName, NameStyleBproto + +import math +from copy import deepcopy + + +class Bitfield(AbstractProtocolComponent): + """Represents a bitfield in a protocol. + This should not be instantiated directly, but rather through the FactoryBitfield class. + Inherit from AbstractProtocolComponent. + """ + + def __init__(self, name: ComponentName, inline: bool = False): + """Initialize a Bitfield object; should not be used directly. + This methode should only be called by the FactoryBitfield class. + + Args: + name (ComponentName): Name of the bitfield, should be unique + length (int): Length of the bitfield in bits. + inline: If True mark this bitfield as a inline definition + """ + self.name: ComponentName = name + self.length: int = 0 + self.bits: dict[ComponentName, int] = {} + self.inline: bool = inline + + # Inherited from AbstractProtocolComponent + def get_identifier(self): + return self.name + + def get_name(self): + return self.name + + def get_type_name(self): + return "bitfield" + + def get_size_bits(self): + return self.length + + def get_size_bytes(self): + return max(math.ceil(self.get_size_bits() / 8), 1) + + # Methodes specific to this class + def apply_naming(self, naming: str): + """Apply a naming scheme to the bitfield. + TODO: Is this method needed? REFACTOR!!! + """ + self.name = f"{naming}_{self.name}" + + def __repr__(self): + field_names = ", ".join([f"{name}:{pos}" for name, pos in self.bits.items()]) + return f"Bitfield({self.name}, {field_names})" + + def __deepcopy__(self, memo={}): + if id(self) in memo: + return memo[id(self)] + + new_bitfield = Bitfield( + deepcopy(self.name, memo), + deepcopy(self.inline, memo) + ) + new_bitfield.length = deepcopy(self.length, memo) + new_bitfield.bits = { + deepcopy(k, memo): deepcopy(v, memo) + for k, v in self.bits.items() + } + + memo[id(self)] = new_bitfield + memo[id(new_bitfield)] = new_bitfield + return new_bitfield + + +class FactoryBitfield: + """Factory class to create Bitfield objects. + Used to assemble a Bitfield object from its parts. + + This class should be used to create Bitfield objects. + Its a one-time use class, so create a new instance for each Bitfield object. + + This Factory is ment to be used during traversal of the abstract syntax tree. + + Simular to the FactoryEnumeration class. + """ + + def __init__(self): + self.bits_list: list[tuple[str, int | None]] = [] + + def add_bit(self, name: str, position: int | None): + """Add a bit to the bitfield. + Used to build the Bitfield object. + + Duplication checks are not done here, but in the assemble + + Args: + name (str): Name of the bit + position (int | None): Position of the bit in the bitfield. If None, the position will be filled in later. + """ + self.bits_list.append((NameStyleBproto.fromStr(name), position)) + + @staticmethod + def calculate_bitsize(bits: dict[int, str]): + """Given a dictionary of bit names and their positions, calculate the bitsize of the bitfield. + Its just a wrapper around the max methode to be honest. + + Args: + bits (dict[int, str]): Dictionary of bit names and their positions + + Returns: + int: Number of bits needed to represent the bit + """ + return max(bits.values(), default=0) + 1 + + def assemble(self, name: str, inline: bool = False): + """Finalize the Bitfield object and return it. + This method should be called after adding all bits to the bitfield. + This will: + 1. Check for duplicate names and positions + 2. Fill in missing positions + 3. Calculate the bitsize of the bitfield + 4. Sort the dictionary by value + + (Very simular to the FactoryEnumeration.assemble method) + + Args: + name (str): Name of the bitfield, should be unique + inline (bool): If true marks the resulting bitfield as inline + + Raises: + BprotoDuplicateBitfieldPositionError: Thrown when a bitfield position is duplicated + BprotoDuplicateNameError: Thrown when a bitfield name is duplicated + + Returns: + _type_: The finalized Bitfield object + """ + resulting_bitfield = Bitfield(NameStyleBproto.fromStr(name), inline=inline) + bits: dict[ComponentName, int | None] = {} + + bit_names: set[ComponentName] = set() + bit_positions = set() + + # Check for duplicates in name or value + for name, pos in self.bits_list: + if pos is not None and pos in bit_positions: + raise BprotoDuplicateBitfieldPositionError(pos, resulting_bitfield) + if name in bit_names: + raise BprotoDuplicateNameError(name, resulting_bitfield) + + if pos is not None: + bit_positions.add(pos) + + bits[name] = pos + bit_names.add(name) + + # Fill in missing values + for name, pos in self.bits_list: + if pos is None: + pos = find_smallset_next_value(bit_positions) + bit_positions.add(pos) + bits[name] = pos + + # Sort the dictionary by value + resulting_bitfield.length = FactoryBitfield.calculate_bitsize(bits) + resulting_bitfield.bits = sort_dict_by_value(bits) + + return resulting_bitfield diff --git a/src/protocol_components/crc.py b/src/protocol_components/crc.py new file mode 100644 index 0000000..595f08c --- /dev/null +++ b/src/protocol_components/crc.py @@ -0,0 +1,22 @@ +CRC_SIZE = 2 + + +def crc16_calc_inital(data: bytearray) -> int: + """CRC-16/CITT-FALSE + from https://stackoverflow.com/questions/35205702/calculating-crc16-in-python + + Args: + data (bytearray): protocol hash + + Returns: + bytes: initals for crc16_calc_with_initial + """ + crc = 0xFFFF + for i in range(0, len(data)): + crc ^= data[i] << 8 + for j in range(0, 8): + if (crc & 0x8000) > 0: + crc = (crc << 1) ^ 0x1021 + else: + crc = crc << 1 + return crc & 0xFFFF diff --git a/src/protocol_components/dtypes.py b/src/protocol_components/dtypes.py new file mode 100644 index 0000000..c8ed7d9 --- /dev/null +++ b/src/protocol_components/dtypes.py @@ -0,0 +1,153 @@ +import enum +from . import AbstractProtocolComponent +from errors import BprotoFrontendError + + +class BprotoFieldBaseType(enum.Enum): + BOOL = "bool" + CHAR = "char" + STRING = "string" + + FLOAT32 = "float32" + FLOAT64 = "float64" + + INT8 = "int8" + INT16 = "int16" + INT32 = "int32" + INT64 = "int64" + + UINT8 = "uint8" + UINT16 = "uint16" + UINT32 = "uint32" + UINT64 = "uint64" + + ENUM = "enum" + BITFIELD = "bitfield" + + +class ArrayPolicy(enum.Enum): + """Enum respresenting the options for policy, regarding if an datatype is an array or not. + Options: DISALLOW, ALLOW, FORCE + """ + DISALLOW = 0 + ALLOW = 1 + FORCE = 2 + + +DTYPE_SIZE_MAP = { + BprotoFieldBaseType.UINT8: 1, + BprotoFieldBaseType.UINT16: 2, + BprotoFieldBaseType.UINT32: 4, + BprotoFieldBaseType.UINT64: 8, + + BprotoFieldBaseType.INT8: 1, + BprotoFieldBaseType.INT16: 2, + BprotoFieldBaseType.INT32: 4, + BprotoFieldBaseType.INT64: 8, + + BprotoFieldBaseType.FLOAT32: 4, + BprotoFieldBaseType.FLOAT64: 8, + + BprotoFieldBaseType.BOOL: 1, + BprotoFieldBaseType.CHAR: 1, + BprotoFieldBaseType.STRING: 1, +} + +DTYPE_ARRAY_POLICY_MAP = { + BprotoFieldBaseType.UINT8: ArrayPolicy.ALLOW, + BprotoFieldBaseType.UINT16: ArrayPolicy.ALLOW, + BprotoFieldBaseType.UINT32: ArrayPolicy.ALLOW, + BprotoFieldBaseType.UINT64: ArrayPolicy.ALLOW, + + BprotoFieldBaseType.INT8: ArrayPolicy.ALLOW, + BprotoFieldBaseType.INT16: ArrayPolicy.ALLOW, + BprotoFieldBaseType.INT32: ArrayPolicy.ALLOW, + BprotoFieldBaseType.INT64: ArrayPolicy.ALLOW, + + BprotoFieldBaseType.FLOAT32: ArrayPolicy.ALLOW, + BprotoFieldBaseType.FLOAT64: ArrayPolicy.ALLOW, + + BprotoFieldBaseType.BOOL: ArrayPolicy.ALLOW, + BprotoFieldBaseType.CHAR: ArrayPolicy.ALLOW, + BprotoFieldBaseType.STRING: ArrayPolicy.FORCE, + + BprotoFieldBaseType.ENUM: ArrayPolicy.DISALLOW, + BprotoFieldBaseType.BITFIELD: ArrayPolicy.DISALLOW, +} + + +def validate_datatype(dtype: str, array_size: int, field: AbstractProtocolComponent) -> tuple[BprotoFieldBaseType, int]: + """Given a string and array size, validate the datatype and array size. + Valid datatypes are defined in BprotoFieldBaseType enum. + Array size is validated based on the datatype and the array policy. + + Args: + dtype (str): string representation of the datatype + array_size (int): size of the array + field (AbstractProtocolComponent): Datatypes parent field, for error reporting + + Raises: + BprotoUnknownDataTypeError: Throws when the datatype is unknown + ValueError: Thrown when something went wrong, this should not happen, like an unimplemented array policy. This is not user error, but a developer error. + BprotoArrayPolicyViolationError: When array policy is violated: E.g. an Datatype is declared as an array, but the policy is disallowing it. + + Returns: + tuple[BprotoFieldBaseType,int]: (datatype as enum, array size) + """ + + if dtype not in BprotoFieldBaseType: + raise BprotoUnknownDataTypeError(dtype) + + dtype: BprotoFieldBaseType = BprotoFieldBaseType(dtype) + + array_policy = DTYPE_ARRAY_POLICY_MAP.get(dtype, None) + + if array_policy is None: + raise ValueError("Internal Error: Array policy for this type was not found, fix this!") + + match(array_policy): + case ArrayPolicy.ALLOW: + pass + case ArrayPolicy.DISALLOW: + if array_size > 1: + raise BprotoArrayPolicyViolationError(array_policy, dtype, field) + case ArrayPolicy.FORCE: + # This is fine, an array size of 1 is allowed + pass + case _: + raise ValueError("Internal Error: Array policy not implemented!") + + return dtype, array_size + + +# Expections +class BprotoUnknownDataTypeError(BprotoFrontendError): + """Error raise when building a field and the datatype is unknown. + This error should be use during the frontend compilation state. + + Inherited from BprotoFrontendError. + """ + def __init__(self, data_type: str): + self.data_type = data_type + super().__init__(f"Unknown data type {data_type}!") + + +class BprotoArrayPolicyViolationError(BprotoFrontendError): + """Error raise when building a field and the array policy is violated. + This error should be use during the frontend compilation state. + + Inherited from BprotoFrontendError. + """ + def __init__(self, policy: ArrayPolicy, dtype: BprotoFieldBaseType, field: AbstractProtocolComponent): + self.policy = policy + self.field = field + + match(policy): + case ArrayPolicy.ALLOW: + raise ValueError("Internal Error: Array policy ALLOW should not be violated!") + case ArrayPolicy.FORCE: + super().__init__(f"{field.get_name()} of type {dtype} must be an array!") + case ArrayPolicy.DISALLOW: + super().__init__(f"{field.get_name()} of type {dtype} must not be an array!") + case _: + raise ValueError("Internal Error: Array policy not implemented!") diff --git a/src/protocol_components/enumeration.py b/src/protocol_components/enumeration.py new file mode 100644 index 0000000..626e99d --- /dev/null +++ b/src/protocol_components/enumeration.py @@ -0,0 +1,232 @@ +from . import AbstractProtocolComponent, find_smallset_next_value, sort_dict_by_value +from .dtypes import DTYPE_SIZE_MAP, BprotoFieldBaseType +from errors import BprotoDuplicateNameError, BprotoDuplicateEnumValueError, BprotoEnumBitsizeTooLargeError +from nameHandling.base import ComponentName, NameStyleBproto + +import math +from copy import deepcopy + + +class Enumeration(AbstractProtocolComponent): + """Represents an enumeration in a protocol. + This should not be instantiated directly, but rather through the FactoryEnumeration class. + + Inherit from AbstractProtocolComponent. + """ + + def __init__(self, name: ComponentName, bitsize: int = 0, inline: bool = False): + """Initialize an Enumeration object; should not be used directly. + + This methode should only be called by the FactoryEnumeration class. + + Args: + name (ComponentName): Name of the enumeration, should be unique + bitsize (int, optional): How many bits are need to represent the largest value? Defaults to 0. + inline (bool): If true marks this enum as inline defintion for later renaming + """ + self.name: ComponentName = name + self.values: dict[ComponentName, int] = {} + self.bitsize: int = bitsize + self.inline: bool = inline + + def __deepcopy__(self, memo={}): + if id(self) in memo: + return memo[id(self)] + + new_enum = Enumeration( + self.name, + self.bitsize, + self.inline + ) + + new_enum.name = deepcopy(self.name, memo) + new_enum.bitsize = deepcopy(self.bitsize, memo) + new_enum.inline = deepcopy(self.inline, memo) + + new_enum.values = deepcopy(self.values, memo) + + memo[id(self)] = new_enum + return new_enum + + # Inherited from AbstractProtocolComponent + def get_identifier(self): + return self.name + + def get_name(self): + return self.name + + def get_type_name(self): + return "enum" + + def get_size_bits(self): + """Returns the bits used to represent the enumeration. + + Returns: + int: Number of bits used to represent the enumeration + """ + return self.bitsize + + def get_size_bytes(self): + """Return the number of bytes needed to represent the enumeration. + + Simular to get_bitsize, but returns the number of bytes. + Formula: ceil(bitsize / 8) + + Returns: + int: Number of bytes needed to represent the enumeration + """ + return math.ceil(self.bitsize / 8) + + # Methodes specific to this class + def apply_naming(self, naming: str): + """TODO: Is this method needed? REFACTOR!!! + + Args: + naming (str): _description_ + """ + self.name = f"{naming}_{self.name}" + + def map_auxiliary_datatypes(self) -> BprotoFieldBaseType: + """Return the bproto datatype that best represents the enumeration. + + There is a theoretical limit of 64 bits for an enumeration. + TODO: Do we need to handle this case? Probably... + + Returns: + BprotoFieldBaseType: Datatype to represent the enumeration + """ + bytes_size = self.get_size_bytes() + + if bytes_size <= 1: + return BprotoFieldBaseType.UINT8 + elif bytes_size <= 2: + return BprotoFieldBaseType.UINT16 + elif bytes_size <= 4: + return BprotoFieldBaseType.UINT32 + elif bytes_size <= 8: + return BprotoFieldBaseType.UINT64 + else: + raise BprotoEnumBitsizeTooLargeError(self) + + def __repr__(self): + value_names = ", ".join([ + f"{name}={val}" + for name, val in self.values.items() + ]) + return f"Enum({self.name}, {value_names})" + + +class FactoryEnumeration: + """Factory class for creating Enumeration objects. + Used to assemble Enumerations from key-value pairs. + + This is a one-time use factory. After calling assemble, the object should not be used anymore. + + This Factory is ment to be used during traversale of the abstract syntax tree. + + Simular to the FactoryBitfield class. + """ + + def __init__(self): + self.key_value_pairs: list[tuple[NameStyleBproto, int]] = [] + + def add_value(self, name: str, value: int | None): + """Add a key-value pair to the enumeration. + Used to build up the enumeration. + + Duplication checks are not done here, but in the assemble + + Args: + name (str): Name of the value + value (int | None): Value of the enumeration. If None, the value will be filled in later. + """ + self.key_value_pairs.append((NameStyleBproto.fromStr(name, "enum"), value)) + + @staticmethod + def calculate_bitsize(values: list[int]) -> int: + """Given a list of unique values, calculate the bitsize needed to represent the largest value/ amount of values. + For empty lists, it will return 1. + + Args: + values (list[int]): List of UNIQUE values, usually a list values of an enumeration + + Returns: + int: Number of bits needed to represent the values + """ + + if len(values) == 0: + return 1 + + max_value = max(values) + + if max_value <= 8589934592: + return math.ceil(math.log2(max_value + 1)) + else: + # This is a bit of a hack, but it works for now + # The problem is that the log2 function is not accurate enough for large numbers + # log2(2**64) = log2(2**64 - 1) = log2(2**64 + 1) = 64 + # And this makes in this case a difference of 1 bit + if max_value <= 18446744073709551615: + return 64 + + return 128 + + def assemble(self, name: ComponentName, inline: bool = False) -> Enumeration: + """Finalize the enumeration and create an Enumeration object. + It uses the key-value pairs added with add_values earlier. + This will: + 1. Check for duplicates in name or value + 2. Fill in missing values + 3. Sort the dictionary by value + 4. Calculate the bitsize of the enumeration and ensure it is within the protocol specs + + After calling this method, the FactoryEnumeration object should not be used anymore. + + Args: + name (ComponentName): Name of the enumeration, should be unique + inline (bool): if True marks this enum as aline + + Raises: + BprotoDuplicateNameError: Error when a name of value is duplicated + BprotoDuplicateEnumValueError: Error when a value is duplicated + BprotoEnumBitsizeTooLargeError: Error when the bitsize of the enumeration is too large for the protocol specs + + Returns: + Enumeration: Finished enumeration object to be used in the backend processing step + """ + enum_dict = {} + used_values = set() + resulting_enum = Enumeration(NameStyleBproto.fromStr(name), 0, inline=inline) + + # Check for duplicates in name or value + for key, value in self.key_value_pairs: + if key in enum_dict: + raise BprotoDuplicateNameError(key, resulting_enum) + if value in used_values and value is not None: + raise BprotoDuplicateEnumValueError(value, resulting_enum) + + if value is not None: + used_values.add(value) + + enum_dict.update({key: value}) + + # Fill in missing values + search_start = 0 + for key, value in enum_dict.items(): + if value is None: + value = find_smallset_next_value(used_values, search_start) + search_start = value + 1 + used_values.add(value) + enum_dict[key] = value + + # Setting a few last properties + resulting_enum.values = sort_dict_by_value(enum_dict) + resulting_enum.bitsize = FactoryEnumeration.calculate_bitsize(list(used_values)) + + # Getting the best datatype to represent the enumeration + # This will raise an error if the enumeration is too large + dtype = resulting_enum.map_auxiliary_datatypes() + + # Mapping datatype to its bitsize + resulting_enum.bitsize = DTYPE_SIZE_MAP[dtype] * 8 + return resulting_enum diff --git a/src/protocol_components/field.py b/src/protocol_components/field.py new file mode 100644 index 0000000..6c76deb --- /dev/null +++ b/src/protocol_components/field.py @@ -0,0 +1,337 @@ +from .dtypes import BprotoFieldBaseType, DTYPE_SIZE_MAP, validate_datatype +from .bitfields import Bitfield +from .enumeration import Enumeration +from . import AbstractProtocolComponent +from nameHandling.base import ComponentName, NameStyleBproto + +from copy import deepcopy + + +class Field(AbstractProtocolComponent): + """Represents a field in a protocol message. + + This object should not be instantiated directly, but rather through the FactoryField class. + This is done during the frontend parsing of the protocol definition. + + Extra classes exist for bitfields and enumerations / their references. + + Inherit from AbstractProtocolComponent. + """ + def __init__(self, + name: ComponentName, + pos: int, + dtype: BprotoFieldBaseType, + array_size: int + ): + """Initialize a Field object; should not be used directly. Use the FactoryField class. + + + Args: + name (ComponentName): Name of the field, should be unique + pos (int): Position of the field in the message + dtype (BprotoFieldBaseType): Datatype of the field, this should already be validated + array_size (int): Size of the array, 1 for scalar fields + """ + self.name: ComponentName = name + self.pos = pos + self.type: BprotoFieldBaseType = dtype + self.array_size: int = array_size + self.ref = None + + def resolve_reference(self, + enum_dict: dict[ComponentName, Enumeration], + bitfield_dict: dict[ComponentName, Bitfield] + ) -> AbstractProtocolComponent: + """_summary_ + + Args: + enum_dict (dict[ComponentName, Enumeration]): Dictoray + bitfield_dict (dict[ComponentName, Bitfield]): _description_ + + Returns: + AbstractProtocolComponent: Return the resolved reference object + """ + return self + + # Inherited from AbstractProtocolComponent + def get_identifier(self) -> ComponentName: + return self.name + + def get_name(self) -> ComponentName: + return self.name + + def get_type_name(self) -> str: + return "field" + + def get_size_bytes(self) -> int: + return DTYPE_SIZE_MAP[self.type] * self.array_size + + def get_size_bits(self) -> int: + return DTYPE_SIZE_MAP[self.type] * self.array_size * 8 + + def get_base_size_bytes(self) -> int: + return DTYPE_SIZE_MAP[self.type] + + # Dunder methods + def __repr__(self): + return f"{self.__class__.__name__}[{self.pos}]({self.name}, {self.type}, {self.array_size})" + (f" ref:{self.ref}" if self.ref is not None else "") + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + + new_field = Field( + deepcopy(self.name, memo), + deepcopy(self.pos, memo), + deepcopy(self.type, memo), + deepcopy(self.array_size, memo) + ) + new_field.ref = deepcopy(self.ref, memo) + + memo[id(self)] = new_field + return new_field + + +# Bitfield Fields + +class FieldBitfield(Field): + """Field object specifically for inline bitfields. + This object should not be instantiated directly, but rather through the FactoryField class. + + Similar to FieldEnum, but for bitfields. + + Inherit from Field and behaves like a Field object. + """ + def __init__(self, name: ComponentName, pos: int, bitfield: Bitfield): + """Should not be used directly. Use the FactoryField class. + + Args: + name (ComponentName): Name of the field, should be unique + pos (int): Position of the field in the message + bitfield (Bitfield): Bitfield object that this field represents + """ + super().__init__(name, pos, BprotoFieldBaseType.BITFIELD, 1) + self.bitfield: Bitfield = bitfield + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + + new_field = FieldBitfield( + deepcopy(self.name, memo), + deepcopy(self.pos, memo), + deepcopy(self.bitfield, memo) + ) + new_field.type = deepcopy(self.type, memo) + new_field.array_size = deepcopy(self.array_size, memo) + + memo[id(self)] = new_field + return new_field + + def get_size_bytes(self) -> int: + return self.bitfield.get_size_bytes() + + def get_size_bits(self): + return self.bitfield.get_bitsize() + + def get_base_size_bytes(self) -> int: + return self.bitfield.get_size_bytes() + + +class FieldBitfieldRef(Field): + """Field object specifically for bitfield references. + This object should not be instantiated directly, but rather through the FactoryField class. + + The reference is the name of the bitfield that this field represents. + This refrence is not validated. + An object of this class should be eventually resolve to a FieldBitfield object. + + Similar to FieldRefEnum, but for bitfield references. + + Inherit from Field and behaves like a Field object. + """ + def __init__(self, name: ComponentName, pos: int, bitfield: ComponentName): + """Should not be used directly. Use the FactoryField class. + + Args: + name (ComponentName): Name of the field, should be unique + pos (int): Position of the field in the message + bitfield (ComponentName): Name of the bitfield that this field references + """ + super().__init__(name, pos, BprotoFieldBaseType.BITFIELD, 1) + self.ref: ComponentName = bitfield + + def __deepcopy__(self, memo): + return super().__deepcopy__(memo) + + def get_size_bytes(self): + return 0 + + def get_size_bits(self): + return 0 + + def get_base_size_bytes(self) -> int: + return 0 + + +# Enum Fields + + +class FieldEnum(Field): + """Field object specifically for inline enumerations. + This object should not be instantiated directly, but rather through the FactoryField class. + + Similar to FieldBitfield, but for enumerations. + + Inherit from Field and behaves like a Field object. + """ + def __init__(self, name: ComponentName, pos: int, enum: Enumeration): + """Should not be used directly. Use the FactoryField class. + + Args: + name (ComponentName): Name of the field, should be unique + pos (int): Position of the field in the message + enum (Enumeration): Enumeration object that this field represents + """ + super().__init__(name, pos, BprotoFieldBaseType.ENUM, 1) + self.enum: Enumeration = enum + + def __deepcopy__(self, memo={}): + if id(self) in memo: + return memo[id(self)] + + new_field = FieldEnum( + deepcopy(self.name, memo), + deepcopy(self.pos, memo), + deepcopy(self.enum, memo) + ) + new_field.type = deepcopy(self.type, memo) + new_field.array_size = deepcopy(self.array_size, memo) + + memo[id(self)] = new_field + return new_field + + def get_size_bytes(self) -> int: + return self.enum.get_size_bytes() + + def get_size_bits(self) -> int: + return self.enum.get_bitsize() + + def get_base_size_bytes(self) -> int: + return self.enum.get_size_bytes() + + +class FieldEnumRef(Field): + """Field object specifically for enumeration references. + This object should not be instantiated directly, but rather through the FactoryField class. + + The reference is the name of the enumeration that this field represents. + This refrence is not validated. + An object of this class should be eventually resolve to a BprotoFieldEnum object. + + Similar to FieldRefBitfield, but for enumerations. + + Inherit from Field and behaves like a Field object. + """ + def __init__(self, name: ComponentName, pos: int, enum: ComponentName): + """Should not be used directly. Use the FactoryField class. + + Args: + name (ComponentName): Name of the field, should be unique + pos (int): Position of the field in the message + enum (ComponentName): Name of the enumeration that this field references + """ + super().__init__(name, pos, BprotoFieldBaseType.ENUM, 1) + self.ref: ComponentName = enum + + def __deepcopy__(self, memo): + return super().__deepcopy__(memo) + + def get_size_bytes(self): + return 0 + + def get_size_bits(self): + return 0 + + def get_base_size_bytes(self) -> int: + return 0 + + +# Factory class +class FactoryField(): + """Factory class for building Field objects representing a bproto field. + This is ment to be used during the frontend compilation stage, during the traversal of the abstract syntax tree. + + Its a one-time use class, so create a new instance for each Field object. + + This class is compleatly statless (yet) + """ + + def __init__(self): + """Initialize the FactoryField object. Nothing to do here yet. + """ + pass + + def assemble(self, + name: str, + pos: int, + dtype: str, + array_size: int, + ref: None | str | Bitfield | Enumeration) -> Field: + """Finalize the field object and return it. Builds Enum and Bitfield Field objects if needed. + + If the datatype is a bitfield or enumeration, the reference object should be provided. + The reference object should be a Bitfield or Enumeration object, or the name of the object. + Acorrding to the reference (and datatype), the correct specialized Field object is created. + Otherwise, the reference should be None. + + If the datatype is a standard type, a Field object is created. + + This methode will: + 1. Validate the datatype and array size (array policy) + 2. Create the correct Field object based on the datatype and reference + + Args: + name (str): Name of the field, should be unique + pos (int): Position of the field in the message + dtype (str): Datatype of the field in string form + array_size (int): Size of the array, 1 for scalar fields + ref (None | str | Bitfield | Enumeration): For bitfields and enumerations, the reference object / reference name + + Raises: + ValueError: Thrown when a reference type is provided that is neither a string nor a Bitfield or Enumeration object, or when an unimplemented array policy is encountered. + BprotoUnknownDataTypeError: Throws when the datatype is unknown + BprotoArrayPolicyViolationError: When array policy is violated: E.g. an Datatype is declared as an array, but the policy is disallowing it. + + Returns: + Field: The finalized Field object, can also be a specialized Field object like FieldBitfield or BprotoFieldEnum + """ + converted_name = NameStyleBproto.fromStr(name) + converted_ref = NameStyleBproto.fromStr(ref) if isinstance(ref, str) else ref + + resulting_field = Field( + converted_name, + pos, + dtype, + array_size + ) + dtype, array_size = validate_datatype(dtype, array_size, resulting_field) + + if dtype == BprotoFieldBaseType.BITFIELD: + if isinstance(ref, Bitfield): + resulting_field = FieldBitfield(converted_name, pos, ref) + elif isinstance(ref, str): + resulting_field = FieldBitfieldRef(converted_name, pos, converted_ref) + else: + raise ValueError("Internal Error: Invalid reference type for bitfield") + elif dtype == BprotoFieldBaseType.ENUM: + if isinstance(ref, Enumeration): + resulting_field = FieldEnum(converted_name, pos, ref) + elif isinstance(ref, str): + resulting_field = FieldEnumRef(converted_name, pos, converted_ref) + else: + raise ValueError("Internal Error: Invalid reference type for enum") + else: + resulting_field = Field(converted_name, pos, dtype, array_size) + + return resulting_field diff --git a/src/protocol_components/message.py b/src/protocol_components/message.py new file mode 100644 index 0000000..00887b1 --- /dev/null +++ b/src/protocol_components/message.py @@ -0,0 +1,144 @@ +from .field import Field +from . import AbstractProtocolComponent +from nameHandling.base import ComponentName, NameStyleBproto +from errors import BprotoDuplicateNameError, BprotoMessageIDAlreadyUsed + +from copy import deepcopy +from collections import OrderedDict + + +class Message(AbstractProtocolComponent): + """Representation of a bproto message. + Should not be instantiated directly, but rather through the FactoryMessage class. + + Contains a dict of fields, where the key is the field name and the value is the Field object. + + Inherit from AbstractProtocolComponent + """ + def __init__(self, name: ComponentName, index_number: int): + """Should not be used directly. Use the FactoryMessage class. + + Args: + name (str): Name of the message, should be unique + index_number (int): Index number of message, used for ordering, should be unique + """ + # field: {name: (type, array_size)} + self.name: ComponentName = name + self.message_index_number = index_number + + self.fields: dict[ComponentName, Field] = {} + + def __deepcopy__(self, memo={}): + if id(self) in memo: + return memo[id(self)] + + new_message = Message( + deepcopy(self.name, memo), + deepcopy(self.message_index_number, memo) + ) + new_message.fields = { + deepcopy(k, memo): deepcopy(v, memo) + for k, v in self.fields.items() + } + + memo[id(self)] = new_message + return new_message + + # Inherited from AbstractProtocolComponent + def get_identifier(self): + return self.message_index_number + + def get_name(self): + return self.name + + def get_type_name(self): + return "message" + + def get_size_bytes(self) -> int: + return sum([field.get_size_bytes() for field in self.fields.values()]) + + def get_size_bits(self) -> int: + return sum([field.get_size_bits() for field in self.fields.values()]) + + # Methodes specific to this class + def apply_naming(self, naming: str): + """Refactor this, should not be used. + + Args: + naming (str): _description_ + """ + self.name = f"{naming}_{self.name}" + + +class FactoryMessage(): + """Factory class for build Message objects representing a bproto message. + This is ment to be used during the frontend compilation stage, during the traversal of the abstract syntax tree. + + Fields are added to the message using the add_field method. + The message is finalized using the assemble method. + After that the Factory should not be used anymore. + Its a one-time use class, so create a new instance for each Message object. + + This class should be used to create Message objects. + """ + + def __init__(self): + self.fields: list[Field] = [] + + def add_field(self, field: Field): + """Adds a finished field to the message. + + This does not check for duplications, this is done in the assemble method. + + Args: + field (BprotoField): The field to add to the message. + """ + self.fields.append(field) + + @staticmethod + def sort_fields_dict(fields: dict[ComponentName, Field]) -> dict[ComponentName, Field]: + """Static methode for sorting a dictionary of bproto fields by their position. + + Args: + fields (dict[ComponentName, BprotoField]): The fields to sort. + + Returns: + dict[ComponentName, BprotoField]: The sorted fields. + """ + return OrderedDict(sorted(fields.items(), key=lambda x: x[1].pos)) + + def assemble(self, name: ComponentName, index_number: int) -> Message: + """Finalize the message and create the Message object, returning it. + After this method is called, the Factory should not be used anymore. + + Args: + name (ComponentName): Name of the message, should be unique. (No uniqueness checks are done here) + index_number (int): Index number of message, used for ordering, should be unique. (No uniqueness checks are done here) + + Raises: + BprotoDuplicateNameError: Raise if a field name is used more than once. + + Returns: + BprotoMessage: The finished message object. + """ + resulting_message = Message(NameStyleBproto.fromStr(name), index_number) + + field_names: set[ComponentName] = set() + field_positions: set[int] = set() + fields_dict: dict[ComponentName, Field] = {} + + for i in self.fields: + if i.name in field_names: + raise BprotoDuplicateNameError(i.name, resulting_message) + + if i.pos in field_positions: + raise BprotoMessageIDAlreadyUsed(i.pos, resulting_message) + + field_positions.add(i.pos) + field_names.add(i.name) + fields_dict[i.name] = i + + fields_dict = FactoryMessage.sort_fields_dict(fields_dict) + resulting_message.fields = fields_dict + + return resulting_message diff --git a/src/protocol_components/protocol.py b/src/protocol_components/protocol.py new file mode 100644 index 0000000..396e75d --- /dev/null +++ b/src/protocol_components/protocol.py @@ -0,0 +1,57 @@ +from protocol_components.message import Message +from protocol_components.bitfields import Bitfield +from protocol_components.enumeration import Enumeration +from nameHandling.base import ComponentName + +from copy import deepcopy + + +class ProtocolDefinitions(): + """Incapuslates all the components of a protocol definition + """ + def __init__(self, name: ComponentName, version: int, + enums: dict[ComponentName, Enumeration], + bitfields: dict[ComponentName, Bitfield], + messages: dict[ComponentName, Message] + ): + """Initializes the ProtocolDefinitions class; Should not be called directly + Use FactoryProtocolDefition.assemble instead + + Args: + name (ComponentName): Name of the protocol + version (int): Version of the protocol + enums (dict[ComponentName, Enumeration]): Enums of the protocol + bitfields (dict[ComponentName, Bitfield]): Bitfields of the protocol + messages (dict[ComponentName, Message]): Messages of the protocol + """ + self.name: ComponentName = name + self.version = version + self.enums = enums + self.bitfields = bitfields + self.messages = messages + self.protocol_hash_inital: None | int = None + + def __deepcopy__(self, memo={}): + if id(self) in memo: + return memo[id(self)] + + res = ProtocolDefinitions( + deepcopy(self.name, memo), + deepcopy(self.version, memo), + enums={ + deepcopy(k, memo): deepcopy(v, memo) + for k, v in self.enums.items() + }, + bitfields={ + deepcopy(k, memo): deepcopy(v, memo) + for k, v in self.bitfields.items() + }, + messages={ + deepcopy(k, memo): deepcopy(v, memo) + for k, v in self.messages.items() + } + ) + res.protocol_hash_inital = self.protocol_hash_inital + + memo[id(self)] = res + return res diff --git a/src/protocol_components/protocolFactory.py b/src/protocol_components/protocolFactory.py new file mode 100644 index 0000000..b6bff44 --- /dev/null +++ b/src/protocol_components/protocolFactory.py @@ -0,0 +1,107 @@ +from protocol_components.message import Message +from protocol_components.bitfields import Bitfield +from protocol_components.enumeration import Enumeration +from protocol_components.protocol import ProtocolDefinitions +from protocol_components import AbstractProtocolComponent +from nameHandling.resolver import ProtocolReferenceResolver +from errors import BprotoAlreadyDefinedError +from nameHandling.base import ComponentName, NameStyleBproto + +from collections import OrderedDict + + +class FactoryProtocolDefition: + """Factory class for creating ProtocolDefinitions + This a one-time use class, once the ProtocolDefinitions object is created, the factory should not be used anymore + """ + + def __init__(self): + """Initializes the FactoryProtocolDefition class; Nothing special here + """ + self.bitfields: dict[ComponentName, Bitfield] = {} + self.enums: dict[ComponentName, Enumeration] = {} + self.messages: dict[ComponentName, Message] = {} + + def add_message(self, message: Message): + """Add a message to the factory, name must be unique + + Args: + message (Message): message to add + + Raises: + BprotoAlreadyDefinedError: Thrown if message name was already defined + """ + if message.name in self.messages: + raise BprotoAlreadyDefinedError(message) + self.messages.update({message.name: message}) + + def add_bitfield(self, bitfield: Bitfield): + """Add a bitfield to the factory, name must be unique + + Args: + bitfield (Bitfield): Bitfield to add with unique name + + Raises: + BprotoAlreadyDefinedError: Thrown if bitfield name was already defined + """ + if bitfield.name in self.bitfields: + raise BprotoAlreadyDefinedError(bitfield) + self.bitfields.update({bitfield.name: bitfield}) + + def add_enum(self, enum: Enumeration): + """Add an enumeration to the factory, name must be unique + + Args: + enum (Enumeration): Enumeration to add with unique name + + Raises: + BprotoAlreadyDefinedError: Thrown if enum name was already defined + """ + if enum.name in self.enums: + raise BprotoAlreadyDefinedError(enum) + self.enums.update({enum.name: enum}) + + @staticmethod + def sort_components(components: dict[str, AbstractProtocolComponent]) -> dict[str, AbstractProtocolComponent]: + """Sorts the components of a protocol definitiown by their identifier (can be integer or string) + + Args: + components (dict[str, AbstractProtocolComponent]): Components, dictionary of messages + + Returns: + dict[str,AbstractProtocolComponent]: Sorted dictionary of components, is a OrderedDict + """ + return OrderedDict(sorted(components.items(), key=lambda x: x[1].get_identifier())) + + def assemble(self, name: str, version: int) -> ProtocolDefinitions: + """Finalizes the ProtocolDefinitions object and returns it + After calling this method, the factory should not be used anymore + + This: + 1. Resolves refrence fields + 2. Sorts the messages + 3. Creates the ProtocolDefinitions object + + Args: + name (str): Name of the protocol, gets converted to ComponentName + version (int): Version of the protocol + + Returns: + ProtocolDefinitions: final ProtocolDefinitions object + """ + + self.messages = self.sort_components(self.messages) + + resulting_protocol_defition = ProtocolDefinitions( + NameStyleBproto.fromStr(name, "protocol_name"), + version, + self.enums, + self.bitfields, + FactoryProtocolDefition.sort_components(self.messages) + ) + + resolver = ProtocolReferenceResolver(resulting_protocol_defition) + resolver.resolve_refrence_protocol() + resolver.back_resolve_inlines() + + return resulting_protocol_defition diff --git a/template/c/template/bproto.cmake.jinja2 b/template/c/template/bproto.cmake.jinja2 new file mode 100644 index 0000000..3150979 --- /dev/null +++ b/template/c/template/bproto.cmake.jinja2 @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.10) + +# Add the library +add_library(bproto STATIC + ${CMAKE_CURRENT_LIST_DIR}/src/{{protocol.name}}_bitfield.c + ${CMAKE_CURRENT_LIST_DIR}/src/{{protocol.name}}_crc.c + ${CMAKE_CURRENT_LIST_DIR}/src/{{protocol.name}}_mapping.c + ${CMAKE_CURRENT_LIST_DIR}/src/{{protocol.name}}_message_fromBytes.c + ${CMAKE_CURRENT_LIST_DIR}/src/{{protocol.name}}_message_toBytes.c +) + +target_include_directories(bproto PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/include +) diff --git a/template/c/template/include/bitfield/bitfield.h.jinja2 b/template/c/template/include/bitfield/bitfield.h.jinja2 new file mode 100644 index 0000000..96c2e6c --- /dev/null +++ b/template/c/template/include/bitfield/bitfield.h.jinja2 @@ -0,0 +1,18 @@ +#pragma once +#include +#include +{% for bitfield in bitfields %} +#define {{bitfield.size_name}} {{bitfield.size}} +{%- endfor %} +{% for bitfield in bitfields %} +typedef struct {{bitfield.name}} { +{%- for bit in bitfield.bits %} + unsigned int {{bit.name}} : {{bit.length}}; +{%- endfor %} +} {{bitfield.name}}; +{% endfor -%} +{% for bitfield in bitfields %} +size_t fromBytes_{{bitfield.name}}(uint8_t * data, {{bitfield.name}} * bitfield); + +size_t toBytes_{{bitfield.name}}(uint8_t * data, {{bitfield.name}} * bitfield); +{% endfor %} \ No newline at end of file diff --git a/template/c/template/include/crc.h.jinja2 b/template/c/template/include/crc.h.jinja2 new file mode 100644 index 0000000..41d7a0f --- /dev/null +++ b/template/c/template/include/crc.h.jinja2 @@ -0,0 +1,18 @@ + +#ifndef _CRC16_H_ +#define _CRC16_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +uint16_t {{protocol.name}}_crc16_ccitt(const uint8_t* buffer, size_t size); + +#ifdef __cplusplus +} +#endif + +#endif // _CRC16_H_ \ No newline at end of file diff --git a/template/c/template/include/enums.h.jinja2 b/template/c/template/include/enums.h.jinja2 new file mode 100644 index 0000000..78bd8c5 --- /dev/null +++ b/template/c/template/include/enums.h.jinja2 @@ -0,0 +1,26 @@ +#pragma once +typedef enum { + {{protocol.name}}_PARSER_RESULT_OK, + {{protocol.name}}_PARSER_RESULT_ERROR_INVALID_SIZE, + {{protocol.name}}_PARSER_RESULT_ERROR_INVALID_ID, + {{protocol.name}}_PARSER_RESULT_ERROR_INVALID_CRC, +} {{protocol.name}}_PARSER_RESULT; + +typedef enum { +{%- for msg in messages %} + {{msg.id_name}} = {{msg.id}}{% if not loop.last %},{% endif %} +{%- endfor %} +} {{protocol.name}}_MSG_ID ; + +typedef enum { +{%- for msg in messages %} + {{msg.size_name}} = {{msg.size}}{% if not loop.last %},{% endif %} +{%- endfor %} +} {{protocol.name}}_MSG_SIZE; +{% for enum in enums %} +typedef enum { +{%- for v in enum.consts %} + {{ v[0] }} = {{ v[1] }}{% if not loop.last %},{% endif %} +{%- endfor %} +} {{enum.name}}; +{% endfor %} \ No newline at end of file diff --git a/template/c/template/include/message/message.h.jinja2 b/template/c/template/include/message/message.h.jinja2 new file mode 100644 index 0000000..1f8ffaa --- /dev/null +++ b/template/c/template/include/message/message.h.jinja2 @@ -0,0 +1,27 @@ +#pragma once +#include "{{protocol.name}}_bitfield.h" +#include "{{protocol.name}}_enum.h" +#include "{{protocol.name}}_crc.h" +#include +#include + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define MAX_MESSAGE_STRUCT_SIZE {{ protocol.max_struct_size_expr }} +#define MAX_MESSAGE_DATA_SIZE {{ protocol.max_data_size }} +{% for msg in messages %} +typedef struct {{msg.name}} { +{%- for field in msg.fields %} + {{ field.type.name }} {{ field.name }}{{ field.type.array }}; +{%- endfor %} +} {{msg.name}}; +{% endfor %} + +{%- for msg in messages %} +size_t toBytes_{{msg.name}}({{msg.name}} * msg, uint8_t * data); + +{{protocol.name}}_PARSER_RESULT fromBytes_{{msg.name}}({{msg.name}} * msg, uint8_t * data, size_t data_size, size_t * parsed_size); +{% endfor %} + +{{protocol.name}}_PARSER_RESULT fromBytes(void * msg, uint8_t * data, size_t data_size, size_t * parsed_size, {{protocol.name}}_MSG_ID * msg_id); + +{{protocol.name}}_MSG_SIZE {{protocol.name}}_mapMessageToSize(uint8_t msg_id); diff --git a/template/c/template/src/bitfield.c.jinja2 b/template/c/template/src/bitfield.c.jinja2 new file mode 100644 index 0000000..a067042 --- /dev/null +++ b/template/c/template/src/bitfield.c.jinja2 @@ -0,0 +1,6 @@ +#include "{{protocol.name}}_bitfield.h" +{% for bitfield in bitfields %} +{% include 'c/template/src/bitfield/fromBytes.c.jinja2' %} + +{% include 'c/template/src/bitfield/toBytes.c.jinja2' %} +{% endfor %} \ No newline at end of file diff --git a/template/c/template/src/bitfield/fromBytes.c.jinja2 b/template/c/template/src/bitfield/fromBytes.c.jinja2 new file mode 100644 index 0000000..b397f25 --- /dev/null +++ b/template/c/template/src/bitfield/fromBytes.c.jinja2 @@ -0,0 +1,10 @@ +size_t fromBytes_{{bitfield.name}}(uint8_t * data, {{bitfield.name}} * bitfield) { + {%- for bit in bitfield.bits %} + {%- if bit.spacer %} + // {{ bit.length }} bits of nothingness + {%- else %} + bitfield->{{ bit.name }} = (data[{{ bit.byte_pos }}] >> {{ bit.bit_pos }}) & 0b1; + {%- endif -%} + {% endfor %} + return {{bitfield.size}}; +} \ No newline at end of file diff --git a/template/c/template/src/bitfield/toBytes.c.jinja2 b/template/c/template/src/bitfield/toBytes.c.jinja2 new file mode 100644 index 0000000..c84acc9 --- /dev/null +++ b/template/c/template/src/bitfield/toBytes.c.jinja2 @@ -0,0 +1,12 @@ +size_t toBytes_{{bitfield.name}}(uint8_t * data, {{bitfield.name}} * bitfield) { + memset(data, 0, {{bitfield.size}}); + {%- for bit in bitfield.bits %} + {%- if bit.spacer %} + // {{ bit.length }} bits of nothingness + {%- else %} + data[{{ bit.byte_pos }}] |= (bitfield->{{ bit.name }} & 0b1) << {{ bit.bit_pos }}; + {%- endif -%} + + {% endfor %} + return {{bitfield.size}}; +} \ No newline at end of file diff --git a/template/c/template/src/crc.c.jinja2 b/template/c/template/src/crc.c.jinja2 new file mode 100644 index 0000000..2c7bad0 --- /dev/null +++ b/template/c/template/src/crc.c.jinja2 @@ -0,0 +1,48 @@ +#include "{{protocol.name}}_crc.h" + +// From: https://gist.github.com/rafacouto/59326c90d6a55f86a3ba + +static const uint16_t {{protocol.name}}_ccitt_hash[] = { + 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7, + 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef, + 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6, + 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de, + 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485, + 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d, + 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4, + 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc, + 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823, + 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b, + 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12, + 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a, + 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41, + 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49, + 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70, + 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78, + 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f, + 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067, + 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e, + 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256, + 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d, + 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405, + 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c, + 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634, + 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab, + 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3, + 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a, + 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92, + 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9, + 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1, + 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8, + 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0, +}; + +uint16_t {{protocol.name}}_crc16_ccitt(const uint8_t* buffer, size_t size) +{ + uint16_t crc = {{protocol.crc_initial}}; + while (size-- > 0) + { + crc = (crc << 8) ^ {{protocol.name}}_ccitt_hash[((crc >> 8) ^ *(buffer++)) & 0x00FF]; + } + return crc; +} \ No newline at end of file diff --git a/template/c/template/src/message/fromBytes.c.jinja2 b/template/c/template/src/message/fromBytes.c.jinja2 new file mode 100644 index 0000000..41f56e4 --- /dev/null +++ b/template/c/template/src/message/fromBytes.c.jinja2 @@ -0,0 +1,70 @@ +#include "{{protocol.name}}_bitfield.h" +#include "{{protocol.name}}_enum.h" +#include "{{protocol.name}}_message.h" +#include "{{protocol.name}}_crc.h" +#include +#include +#include +#include +{% for msg in messages %} +{{protocol.name}}_PARSER_RESULT fromBytes_{{msg.name}}({{msg.name}} * msg, uint8_t * data, size_t data_size, size_t * parsed_size) { + data += 1; +{% for field in msg.fields %} +{%- if field.decompose_mode == "for" %} + for(int i=0; i<{{field.array_size}}; i++) { + {% if field.pre_init_value != None %}msg->{{field.name}}[i] = {{field.pre_init_value}};{% endif %} + memcpy(&msg->{{field.name}}[i], data, sizeof({{field.type.aux or field.type.name}})); + data += {{field.base_size}}; + } +{%- elif field.decompose_mode == "inline_for" %} + {%- for i in range(field.array_size) %} + {% if field.pre_init_value != None %}msg->{{field.name}}[{{i}}] = {{field.pre_init_value}};{% endif %} + {% if field.fromBytes_overwride %}{{ field.fromBytes_overwride }}{% else %}memcpy(&msg->{{field.name}}[{{i}}], data, sizeof({{field.type.aux or field.type.name}}));{% endif %}; + data += {{field.base_size}}; + {%- endfor %} +{%- else %} + {% if field.pre_init_value != None %}msg->{{field.name}} = {{field.pre_init_value}};{% endif %} + {% if field.fromBytes_overwride %}{{ field.fromBytes_overwride }}{% else %}memcpy(&msg->{{field.name}}, data, sizeof({{field.type.aux or field.type.name}}));{% endif %}; + data += {{field.base_size}}; +{%- endif %} +{% endfor %} + *parsed_size = {{msg.size}}; + return {{protocol.name}}_PARSER_RESULT_OK; +} +{% endfor %} + +{{protocol.name}}_PARSER_RESULT fromBytes(void * msg, uint8_t * data, size_t data_size, size_t * parsed_size, {{protocol.name}}_MSG_ID * msg_id) { + if(data_size < 1) { + return {{protocol.name}}_PARSER_RESULT_ERROR_INVALID_SIZE; + } + + uint8_t incomming_msg_id= data[0]; + size_t expected_size = {{protocol.name}}_mapMessageToSize(incomming_msg_id); + + if(expected_size <= 0) { + return {{protocol.name}}_PARSER_RESULT_ERROR_INVALID_ID; + } + + if(data_size < expected_size) { + return {{protocol.name}}_PARSER_RESULT_ERROR_INVALID_SIZE; + } + + uint16_t crc; + memcpy(&crc, data + expected_size - {{ protocol.crc_size }}, sizeof(uint16_t)); + + if(HCP_crc16_ccitt(data, expected_size - {{ protocol.crc_size }}) != crc) { + return {{protocol.name}}_PARSER_RESULT_ERROR_INVALID_CRC; + } + + *msg_id = incomming_msg_id; + + switch(incomming_msg_id) { +{%- for msg in messages %} + case {{msg.id_name}}: + return fromBytes_{{msg.name}}(msg, data, data_size, parsed_size); + break; +{% endfor %} + } + + return {{protocol.name}}_PARSER_RESULT_OK; +} \ No newline at end of file diff --git a/template/c/template/src/message/mapping.c.jinja2 b/template/c/template/src/message/mapping.c.jinja2 new file mode 100644 index 0000000..42d6314 --- /dev/null +++ b/template/c/template/src/message/mapping.c.jinja2 @@ -0,0 +1,14 @@ +#include "{{protocol.name}}_enum.h" +#include + +{{protocol.name}}_MSG_SIZE {{protocol.name}}_mapMessageToSize(uint8_t msg_id) { + switch(msg_id) { +{%- for msg in messages %} + case {{msg.id_name}}: + return {{msg.size_name}}; +{%- endfor %} + default: + return 0; + } +} + diff --git a/template/c/template/src/message/toBytes.c.jinja2 b/template/c/template/src/message/toBytes.c.jinja2 new file mode 100644 index 0000000..1ab5bc6 --- /dev/null +++ b/template/c/template/src/message/toBytes.c.jinja2 @@ -0,0 +1,34 @@ +#include "{{protocol.name}}_bitfield.h" +#include "{{protocol.name}}_enum.h" +#include "{{protocol.name}}_message.h" +#include "{{protocol.name}}_crc.h" +#include +#include +#include +{% for msg in messages %} +size_t toBytes_{{msg.name}}({{msg.name}} * msg, uint8_t * data) { + uint8_t * data_begin = data; + *data = (uint8_t)({{msg.id_name}}); + data += 1; +{% for field in msg.fields %} +{%- if field.decompose_mode == "for" %} + for(int i=0; i<{{field.array_size}}; i++) { + memcpy(data, &msg->{{field.name}}[i], sizeof({{field.type.aux or field.type.name}})); + data += {{field.base_size}}; + } +{%- elif field.decompose_mode == "inline_for" %} + {%- for i in range(field.array_size) %} + {% if field.toBytes_overwride %}{{field.toBytes_overwride}}{% else %}memcpy(data, &msg->{{field.name}}[{{i}}], sizeof({{field.type.aux or field.type.name}}));{% endif %}; + data += {{field.base_size}}; + {%- endfor %} +{%- else %} + {% if field.toBytes_overwride %}{{field.toBytes_overwride}}{% else %}memcpy(data, &msg->{{field.name}}, sizeof({{field.type.aux or field.type.name}}));{% endif %}; + data += {{field.base_size}}; +{%- endif %} +{% endfor %} + uint16_t crc = {{protocol.name}}_crc16_ccitt(data_begin, {{msg.size_name}} - {{protocol.crc_size}}); + memcpy(data, &crc, sizeof(uint16_t)); + + return {{msg.size_name}}; +} +{% endfor %} \ No newline at end of file diff --git a/template/python/static/bproto_base.py b/template/python/static/bproto_base.py new file mode 100644 index 0000000..a8144f2 --- /dev/null +++ b/template/python/static/bproto_base.py @@ -0,0 +1,67 @@ +import abc + + +class bproto_Package(abc.ABC): + @abc.abstractmethod + def to_bytes(self) -> bytes: + raise NotImplementedError() + + @abc.abstractmethod + def from_bytes(self, data: bytes): + raise NotImplementedError() + + +class bproto_Bitfield(abc.ABC): + @abc.abstractmethod + def to_bytes(self) -> bytes: + pass + + @abc.abstractmethod + def from_bytes(self, data: bytes): + pass + + def __str__(self): + return self.__repr__() + + +def crc16_calc_inital(data: bytearray) -> int: + """CRC-16/CITT-FALSE + from https://stackoverflow.com/questions/35205702/calculating-crc16-in-python + + Args: + data (bytearray): protocol hash + + Returns: + bytes: initals for crc16_calc_with_initial + """ + crc = 0xFFFF + for i in range(0, len(data)): + crc ^= data[i] << 8 + for j in range(0, 8): + if (crc & 0x8000) > 0: + crc = (crc << 1) ^ 0x1021 + else: + crc = crc << 1 + return crc + + +def calc_crc_sum(data: bytearray, inital: int) -> bytes: + """CRC-16/CITT-FALSE + form https://stackoverflow.com/questions/35205702/calculating-crc16-in-python + + Args: + data (bytearray): protocol data + inital (int): Inital calculated from protocol hash + + Returns: + bytes: 2 byte CRC Checksum + """ + crc = inital + for i in range(0, len(data)): + crc ^= data[i] << 8 + for j in range(0, 8): + if (crc & 0x8000) > 0: + crc = (crc << 1) ^ 0x1021 + else: + crc = crc << 1 + return crc & 0xFFFF diff --git a/template/python/static/bproto_error.py b/template/python/static/bproto_error.py new file mode 100644 index 0000000..100da2e --- /dev/null +++ b/template/python/static/bproto_error.py @@ -0,0 +1,19 @@ +class bproto_Error(Exception): + def __init__(self, message): + self.message = message + + +class bproto_PackageError(bproto_Error): + pass + + +class bproto_PackageErrorInvalidSize(bproto_PackageError): + pass + + +class bproto_PackageErrorInvalidMessageID(bproto_PackageError): + pass + + +class bproto_PackageErrorInvalidCRC(bproto_PackageError): + pass diff --git a/template/python/template/bproto_protocol_bitfield.py.jinja2 b/template/python/template/bproto_protocol_bitfield.py.jinja2 new file mode 100644 index 0000000..1451cc4 --- /dev/null +++ b/template/python/template/bproto_protocol_bitfield.py.jinja2 @@ -0,0 +1,38 @@ +from bproto_base import bproto_Bitfield + +{% for bitfield in bitfields %} +class {{bitfield.name}}(bproto_Bitfield): + BYTE_SIZE = {{bitfield.size}} + + def __init__(self, + {%- for bit in bitfield.bits %} + {{bit.name}}=False, + {%- endfor %} + ): + {%- for bit in bitfield.bits %} + self.{{bit.name}} = {{bit.name}} + {%- endfor %} + + def to_bytes(self) -> bytes: + res = 0 + {%- for bit in bitfield.bits %} + res |= (1 << {{bit.pos}}) if self.{{bit.name}} else 0 + {%- endfor %} + return int.to_bytes(res, self.BYTE_SIZE, '{{protocol.endian_str}}') + + def from_bytes(self, data: bytes): + bits = int.from_bytes(data[:self.BYTE_SIZE], '{{protocol.endian_str}}') + {%- for bit in bitfield.bits %} + self.{{bit.name}} = (bits & (1 << {{bit.pos}})) != 0 + {%- endfor %} + return self + + def __repr__(self) -> str: + value_str = ", ".join([ + {%- for bit in bitfield.bits %} + "{{bit.name}}=" + str(self.{{bit.name}}), + {%- endfor %} + ]) + return "{{bitfield.name}}(" + value_str + ")" + +{% endfor %} \ No newline at end of file diff --git a/template/python/template/bproto_protocol_packets.py.jinja2 b/template/python/template/bproto_protocol_packets.py.jinja2 new file mode 100644 index 0000000..da224d2 --- /dev/null +++ b/template/python/template/bproto_protocol_packets.py.jinja2 @@ -0,0 +1,19 @@ +from bproto_base import bproto_Package, calc_crc_sum +from bproto_error import bproto_Error, bproto_PackageErrorInvalidSize, bproto_PackageErrorInvalidMessageID, bproto_PackageErrorInvalidCRC + +from {{ import_name }}_protocol_enum import {% for e in enums %}{{e.name}}{% if not loop.last %}, {% endif %}{% endfor %} +from {{ import_name }}_protocol_bitfield import {% for b in bitfields %}{{b.name}}{% if not loop.last %}, {% endif %}{% endfor %} +from {{ import_name }}_protocol_message_ids import MessageIds_{{ protocol.name }}, MESSAGE_SIZE_MAP_{{ protocol.name }} + +from typing import Annotated, List +import struct + +CRC_INITAL = {{ protocol.crc_initial }} + + +{% for m in messages %} +{%- include 'python/template/message/class.jinja2' -%} +{%- if not loop.last %} + +{% else %}{%- endif -%}{%- endfor %} +{% include 'python/template/message/parse_generic.jinja2' -%} \ No newline at end of file diff --git a/template/python/template/bproto_protocol_packets_ids.py.jinja2 b/template/python/template/bproto_protocol_packets_ids.py.jinja2 new file mode 100644 index 0000000..bb9bb87 --- /dev/null +++ b/template/python/template/bproto_protocol_packets_ids.py.jinja2 @@ -0,0 +1,15 @@ +import enum + + +class MessageIds_{{ protocol.name }}(enum.Enum): + {%- for msg in messages %} + {{ msg.id_name }} = {{ msg.id }} + {%- endfor %} + + +MESSAGE_SIZE_MAP_{{ protocol.name }} = { + {%- for msg in messages %} + MessageIds_{{ protocol.name }}.{{ msg.id_name }}: {{ msg.size }}, + {%- endfor %} +} + diff --git a/template/python/template/enum.py.jinja2 b/template/python/template/enum.py.jinja2 new file mode 100644 index 0000000..613cfc2 --- /dev/null +++ b/template/python/template/enum.py.jinja2 @@ -0,0 +1,8 @@ +import enum +{% for e in enums %} + +class {{ e.name }}(enum.Enum): +{%- for k,v in e.consts %} + {{ k }} = {{ v }} +{%- endfor %} +{% endfor %} diff --git a/template/python/template/message/class.jinja2 b/template/python/template/message/class.jinja2 new file mode 100644 index 0000000..77a55f3 --- /dev/null +++ b/template/python/template/message/class.jinja2 @@ -0,0 +1,20 @@ +class {{m.name}}(bproto_Package): + ID = MessageIds_{{ protocol.name }}.{{m.id_name}} + SIZE = {{ m.size }} + {% for f in m.fields %} + {{f.name}}: {{f.type}} + {%- endfor %} + + def __init__(self, + {% for f in m.fields -%} + {{f.name}}: {{f.type}} = {{f.default_value}}, + {% endfor -%} + ): + {% for f in m.fields -%} + self.{{f.name}} = {{f.name}} + {% endfor -%} + + +{% include 'python/template/message/toBytes.jinja2' %} +{% include 'python/template/message/fromBytes.jinja2' %} + diff --git a/template/python/template/message/fromBytes.jinja2 b/template/python/template/message/fromBytes.jinja2 new file mode 100644 index 0000000..3c1e222 --- /dev/null +++ b/template/python/template/message/fromBytes.jinja2 @@ -0,0 +1,21 @@ + + def from_bytes(self, data: bytes): + + if len(data) < self.SIZE: + raise bproto_PackageErrorInvalidSize(f"A size of {len(data)} is to small for {{ protocol.name }} (min. {self.SIZE})") + + msg_id = struct.unpack('{{protocol.endian_format}}B', data[:1])[0] + crc_data = struct.unpack('{{protocol.endian_format}}H', data[-{{ protocol.crc_size }}:])[0] + + if calc_crc_sum(data[:-{{ protocol.crc_size }}], CRC_INITAL) != crc_data: + raise bproto_PackageErrorInvalidCRC(f"CRC {crc_data:04x} did not match calculated") + + if msg_id != self.ID.value: + raise bproto_PackageErrorInvalidMessageID("Message ID {msd_id} does not match {{ protocol.name }}'s id {self.ID.value}") + + data = data[1:] + {% for f in m.fields -%} + self.{{ f.name }} = {{ f.from_bytes_conversion }} + data = data[{{ f.size }}:] + {% endfor -%} + return self \ No newline at end of file diff --git a/template/python/template/message/parse_generic.jinja2 b/template/python/template/message/parse_generic.jinja2 new file mode 100644 index 0000000..d3ec83a --- /dev/null +++ b/template/python/template/message/parse_generic.jinja2 @@ -0,0 +1,31 @@ + +def parse_package(data: bytes) -> tuple[bproto_Package, int]: + if len(data) < 1: + raise bproto_PackageErrorInvalidSize("Package has no data") + + msg_id = struct.unpack('{{protocol.endian_format}}B', data[:1])[0] + + if msg_id not in MessageIds_{{ protocol.name }}: + raise bproto_PackageErrorInvalidMessageID(f"Message ID '{msg_id}' is invaild") + + msg_id = MessageIds_{{ protocol.name }}(msg_id) + expected_size = MESSAGE_SIZE_MAP_{{ protocol.name }}.get(msg_id) or 0 + + if expected_size > len(data): + raise bproto_PackageErrorInvalidSize(f"Package is to short for '{msg_id}' ({expected_size} > {len(data)})") + + pkg_data = data[:expected_size] + + crc_data = struct.unpack('{{protocol.endian_format}}H', pkg_data[-2:])[0] + + if calc_crc_sum(pkg_data[:-2], CRC_INITAL) != crc_data: + raise bproto_PackageErrorInvalidCRC(f"CRC {crc_data:04x} did not match calculated") + + match(msg_id): + {%- for msg in messages %} + case MessageIds_{{ protocol.name }}.{{ msg.id_name }}: + return {{msg.name}}().from_bytes(pkg_data), expected_size + {%- endfor %} + case _: + raise bproto_Error("Message ID could not be interpreted; this should not have happen; its a developer error! Create a issue") + diff --git a/template/python/template/message/toBytes.jinja2 b/template/python/template/message/toBytes.jinja2 new file mode 100644 index 0000000..651bf13 --- /dev/null +++ b/template/python/template/message/toBytes.jinja2 @@ -0,0 +1,8 @@ + + def to_bytes(self) -> bytes: + res = struct.pack("{{protocol.endian_format}}B", self.ID.value) + {% for f in m.fields -%} + res += {{ f.to_bytes_conversion }} + {% endfor -%} + res += struct.pack("{{protocol.endian_format}}H", calc_crc_sum(res, CRC_INITAL)) + return res diff --git a/template/txt/protocolSummary.txt.jinja2 b/template/txt/protocolSummary.txt.jinja2 new file mode 100644 index 0000000..7f1eb43 --- /dev/null +++ b/template/txt/protocolSummary.txt.jinja2 @@ -0,0 +1,26 @@ +Name: {{name}} +Version: {{version}} + +Enums: +{%- for enum in enums %} + {{enum.name}}: + {%- for v in enum.consts %} + - {{v[0]}} = {{v[1]}} + {%- endfor %} +{% endfor %} +Bitfield: +{%- for bitfield in bitfields %} + {{bitfield.name}}: + {%- for v in bitfield.fields %} + - {{v[0]}}: {{v[1]}} + {%- endfor %} +{% endfor %} +Messages: +{%- for message in messages %} + [{{message.id}}] {{message.name}}: + Size: {{message.size}} bytes + Fields: + {%- for field in message.fields %} + - ({{field.size}}) {{field.name}}: {{field.type}}[{{field.array_size}}] {% if field.ref != None %}-> {{field.ref}}{% endif %} + {%- endfor %} +{% endfor -%} \ No newline at end of file diff --git a/test/compiler/ast/test_ast_message.py b/test/compiler/ast/test_ast_message.py new file mode 100644 index 0000000..52718c8 --- /dev/null +++ b/test/compiler/ast/test_ast_message.py @@ -0,0 +1,38 @@ +import sys +import pytest +from typing import Any +sys.path.append('src/') + +from protocol_components.dtypes import BprotoFieldBaseType +from parser.parser import create_ast_parser, bproto_ErrorListener +from parser.ast_visitor import BprotoASTVisitor + + +@pytest.mark.parametrize("source_text,expected_output", [ + ("uint64\n", (BprotoFieldBaseType.UINT64, 1, None)), + ("float32\n", (BprotoFieldBaseType.FLOAT32, 1, None)), + ("float32[8]\n", (BprotoFieldBaseType.FLOAT32, 8, None)), +]) +def test_ast_field_dtype_defintion( + source_text: str, + expected_output: tuple[BprotoFieldBaseType, int, Any]): + + err_listner = bproto_ErrorListener() + parser = create_ast_parser(source_text, err_listner) + ast = parser.dtype() + vinterp = BprotoASTVisitor() + + assert len(err_listner.syntax_error_list) == 0 + + res = vinterp.visit(ast) + assert isinstance(res, tuple) + assert len(res) == 3 + + # Correct dtype + assert res[0] == expected_output[0].value + + # Correct array size + assert res[1] == expected_output[1] + + # Correct refrerence; Should normally be None be Non + assert res[2] == expected_output[2] diff --git a/test/compiler/ast/test_ast_tag_line.py b/test/compiler/ast/test_ast_tag_line.py new file mode 100644 index 0000000..48b399c --- /dev/null +++ b/test/compiler/ast/test_ast_tag_line.py @@ -0,0 +1,23 @@ +import sys +sys.path.append('src/') + +from parser.parser import create_ast_parser, bproto_ErrorListener +from parser.ast_visitor import BprotoASTVisitor + + +def test_ast_tag_line(): + source = "protocol HCP version 25\n" + + err_listner = bproto_ErrorListener() + parser = create_ast_parser(source, err_listner) + ast = parser.protocol_header() + vinterp = BprotoASTVisitor() + + assert len(err_listner.syntax_error_list) == 0 + + res = vinterp.visit(ast) + assert isinstance(res, tuple) + assert len(res) == 2 + name, version = res + assert name == "HCP" + assert version == 25 diff --git a/test/compiler/backend/test_comiler_c.py b/test/compiler/backend/test_comiler_c.py new file mode 100644 index 0000000..6bdce76 --- /dev/null +++ b/test/compiler/backend/test_comiler_c.py @@ -0,0 +1,26 @@ +import sys +from compiler import BprotoCompiler +sys.path.append('src/') + +from errors import BprotoCompilerError +from backend.rendering.cBackend import CBackendRenderer +from backend.fsOutput import BackendFileSystemOutput + + +def test_compiler_c_smoke_test(): + # Load Refernces + with open("test/test_protocol.bproto") as f: + source_text = f.read() + + # Comile Stuff + compiler = BprotoCompiler([ + CBackendRenderer(".") + ], "template") + + try: + output: BackendFileSystemOutput = compiler.compile(source_text, ".") + except BprotoCompilerError: + assert False, "This should run througth" + + output.saveToDisk("test/backend_output/c") + output.toString() diff --git a/test/compiler/backend/test_compiler_python.py b/test/compiler/backend/test_compiler_python.py new file mode 100644 index 0000000..3ec03c3 --- /dev/null +++ b/test/compiler/backend/test_compiler_python.py @@ -0,0 +1,59 @@ +import sys +import pytest +from compiler import BprotoCompiler +sys.path.append('src/') + +from errors import BprotoCompilerError +from backend.rendering.pythonBackend import PythonBackendRenderer +from backend.fsOutput import BackendFileSystemOutput + + +@pytest.mark.skip() +def test_compiler_python_against_ref(): + # Load Refernces + with open("test/test_protocol.bproto") as f: + source_text = f.read() + + # Comile Stuff + compiler = BprotoCompiler([ + PythonBackendRenderer(".") + ], "template") + + try: + output: BackendFileSystemOutput = compiler.compile(source_text, ".") + except BprotoCompilerError: + assert False, "This should run througth" + + output.saveToDisk("test/backend_output/python") + string_output = output.toString() + + with open("test/reference/python/HCP_protocol_enum.py") as f: + assert f.read() == string_output.get("HCP_protocol_enum.py") + + with open("test/reference/python/HCP_protocol_bitfield.py") as f: + assert f.read() == string_output.get("HCP_protocol_bitfield.py") + + with open("test/reference/python/HCP_protocol_message_ids.py") as f: + assert f.read() == string_output.get("HCP_protocol_message_ids.py") + + with open("test/reference/python/HCP_protocol_packets.py") as f: + assert f.read() == string_output.get("HCP_protocol_packets.py") + + +def test_compiler_python_smoke_test(): + # Load Refernces + with open("test/test_protocol.bproto") as f: + source_text = f.read() + + # Comile Stuff + compiler = BprotoCompiler([ + PythonBackendRenderer(".") + ], "template") + + try: + output: BackendFileSystemOutput = compiler.compile(source_text, ".") + except BprotoCompilerError: + assert False, "This should run througth" + + output.saveToDisk("test/backend_output/python") + output.toString() diff --git a/test/compiler/backend/test_compiler_txt.py b/test/compiler/backend/test_compiler_txt.py new file mode 100644 index 0000000..5ec87cf --- /dev/null +++ b/test/compiler/backend/test_compiler_txt.py @@ -0,0 +1,32 @@ +import sys +from compiler import BprotoCompiler +sys.path.append('src/') + +from errors import BprotoCompilerError +from backend.rendering.txtBackend import TxtBackendRenderer +from backend.fsOutput import BackendFileSystemOutput + + +def test_compiler_txt_against_ref(): + with open("test/test_protocol.bproto") as f: + source_text = f.read() + + # Comile Stuff + compiler = BprotoCompiler([ + TxtBackendRenderer(".") + ], "template") + + try: + output: BackendFileSystemOutput = compiler.compile(source_text, ".") + except BprotoCompilerError: + assert False, "This should run througth" + + string_output = output.toString() + output.saveToDisk("test/backend_output/txt") + + # Load Refernces + with open("test/reference/txt/protocolSummary.txt") as f: + expected_output = f.read() + + assert isinstance(string_output.get("protocolSummary.txt"), str) + assert string_output.get("protocolSummary.txt") == expected_output diff --git a/test/compiler/backend/test_fsOutput.py b/test/compiler/backend/test_fsOutput.py new file mode 100644 index 0000000..a05f846 --- /dev/null +++ b/test/compiler/backend/test_fsOutput.py @@ -0,0 +1,168 @@ +import sys +import os +import pytest +import shutil +from jinja2 import Environment, FileSystemLoader, select_autoescape +sys.path.append('src/') + +from backend.fsOutput import BackendFSOutFile, BackendFSOutStaticConent, BackendFSOutFolder, BackendFSOutputJinjaFile + +TEST_FOLDER = "/tmp/protocolGenerator_test_data" + + +@pytest.fixture +def filesystem_folder(): + yield TEST_FOLDER + shutil.rmtree(TEST_FOLDER) + + print("Cleanup done") + + +def test_fsOutput_saveToDisk(filesystem_folder: str): + jinja_env = Environment( + loader=FileSystemLoader("test/data/templates/"), + autoescape=select_autoescape() + ) + + # Setup + root_folder = BackendFSOutFolder("root", [ + BackendFSOutFile("file1", "some content"), + BackendFSOutFile("file2", "some other content"), + BackendFSOutFolder("subfolder", [ + BackendFSOutFile("file3", "some more content #2"), + BackendFSOutFile("file4", "even more content"), + ]), + BackendFSOutStaticConent("test/data/static_folder/static_content2.txt", "static_content2.txt"), + BackendFSOutStaticConent("test/data/static_content1.txt", "static_content1.txt"), + BackendFSOutputJinjaFile(jinja_env, "template.jinja2", "template.txt", {"name": "World"}) + ]) + + root_folder.saveToDisk(filesystem_folder) + + # Test Structure + assert os.path.isdir(filesystem_folder) + assert os.path.isfile(filesystem_folder + "/root/file1") + assert os.path.isfile(filesystem_folder + "/root/file2") + assert os.path.isdir(filesystem_folder + "/root/subfolder") + assert os.path.isfile(filesystem_folder + "/root/subfolder/file3") + assert os.path.isfile(filesystem_folder + "/root/subfolder/file4") + assert os.path.isfile(filesystem_folder + "/root/static_content1.txt") + assert os.path.isfile(filesystem_folder + "/root/static_content2.txt") + + assert os.path.isfile(filesystem_folder + "/root/template.txt") + + # Test Content + + with open(filesystem_folder + "/root/file1", "r") as f: + assert f.read() == "some content" + + with open(filesystem_folder + "/root/file2", "r") as f: + assert f.read() == "some other content" + + with open(filesystem_folder + "/root/subfolder/file3", "r") as f: + assert f.read() == "some more content #2" + + with open(filesystem_folder + "/root/subfolder/file4", "r") as f: + assert f.read() == "even more content" + + with open(filesystem_folder + "/root/static_content1.txt", "r") as f: + assert f.read() == "test1" + + with open(filesystem_folder + "/root/static_content2.txt", "r") as f: + assert f.read() == "test2" + + with open(filesystem_folder + "/root/template.txt", "r") as f: + assert f.read() == "Hello World!" + + +def test_fsOutput_treeString(): + jinja_env = Environment( + loader=FileSystemLoader("test/data/templates/"), + autoescape=select_autoescape() + ) + + root_folder = BackendFSOutFolder("root", [ + BackendFSOutFile("file1", "some content"), + BackendFSOutFile("file2", "some other content"), + BackendFSOutFolder("subfolder", [ + BackendFSOutFile("file3", "some more content #2"), + BackendFSOutFile("file4", "even more content"), + BackendFSOutputJinjaFile(jinja_env, "template.jinja", "generate", {"name": "Jinja2"}) + ]), + BackendFSOutStaticConent("test/data/static_folder", "data"), + BackendFSOutputJinjaFile(jinja_env, "template.jinja", None, {"name": "World"}) + ]) + + string = root_folder.assemble_file_tree_string() + assert string == """└── root + ├── [file1] + ├── [file2] + ├── subfolder + │ ├── [file3] + │ ├── [file4] + │ └── Template [generate] + ├── Static [data] + └── Template [template] +""" + + +def test_fsOutput_toString(): + jinja_env = Environment( + loader=FileSystemLoader("test/data/templates/"), + autoescape=select_autoescape() + ) + + root_folder = BackendFSOutFolder("root", [ + BackendFSOutFile("file1", "some content"), + BackendFSOutFile("file2", "some other content"), + BackendFSOutFolder("subfolder", [ + BackendFSOutFile("file3", "some more content #2"), + BackendFSOutFile("file4", "even more content"), + BackendFSOutputJinjaFile(jinja_env, "template.jinja2", "generate", {"name": "Jinja2"}) + ]), + BackendFSOutStaticConent("test/data/static_folder", "data/static_folder"), + BackendFSOutStaticConent("test/data/static_content1.txt", "data"), + BackendFSOutputJinjaFile(jinja_env, "template.jinja2", None, {"name": "World"}) + ]) + + res = root_folder.toString() + + assert res == { + "root/file1": "some content", + "root/file2": "some other content", + "root/subfolder/file3": "some more content #2", + "root/subfolder/file4": "even more content", + "root/subfolder/generate": "Hello Jinja2!", + "root/data/static_content1.txt": "test1", + "root/data/static_folder/static_content2.txt": "test2", + "root/template": "Hello World!" + } + + +def test_fsOutput_pathnormalization(filesystem_folder: str): + + root_folder = BackendFSOutFolder(".", [ + BackendFSOutFolder(".", [ + BackendFSOutFile("file2", "some other content"), + ]), + BackendFSOutFile("file1", "some content"), + ]) + + # Check if this works for toString + res = root_folder.toString() + assert res == { + "file2": "some other content", + "file1": "some content" + } + + # Check if this works for saveToDisk + root_folder.saveToDisk(filesystem_folder) + + assert os.path.isfile(filesystem_folder + "/file1") + assert os.path.isfile(filesystem_folder + "/file2") + + with open(filesystem_folder + "/file1", "r") as f: + assert f.read() == "some content" + + with open(filesystem_folder + "/file2", "r") as f: + assert f.read() == "some other content" diff --git a/test/compiler/name_conversion/test_name_bproto_style.py b/test/compiler/name_conversion/test_name_bproto_style.py new file mode 100644 index 0000000..0bea56c --- /dev/null +++ b/test/compiler/name_conversion/test_name_bproto_style.py @@ -0,0 +1,49 @@ +import sys +sys.path.append('src/') + +from nameHandling.base import ComponentName, NameStyleBproto + + +def test_component_name(): + assert ComponentName(["test"]) != 1234 + assert ComponentName(["test"]) == ComponentName(["test"]) + assert ComponentName(["test"]) != ComponentName(["test", "test"]) + assert hash(ComponentName(["test"])) == hash(ComponentName(["test"])) + assert hash(ComponentName(["test"])) != hash(ComponentName(["test", "test"])) + assert repr(ComponentName(["test"])) == "ComponentName(Test)" + assert repr(ComponentName(["test", "test"])) == "ComponentName(Test-Test)" + + +def test_ComponentNameStyleBproto(): + assert NameStyleBproto.fromStr("test") == ComponentName(["test"]) + assert NameStyleBproto.fromStr("ThisIsATest") == ComponentName(["this", "is", "a", "test"]) + assert NameStyleBproto.fromStr("thisIsATest") == ComponentName(["this", "is", "a", "test"]) + assert NameStyleBproto.fromStr("this-is-a-test") == ComponentName(["this", "is", "a", "test"]) + assert NameStyleBproto.fromStr("this-Is-A-test") == ComponentName(["this", "is", "a", "test"]) + assert NameStyleBproto.fromStr("this_is_a_test") == ComponentName(["this", "is", "a", "test"]) + assert NameStyleBproto.fromStr("this_Is_a_Test") == ComponentName(["this", "is", "a", "test"]) + assert NameStyleBproto.fromStr("this-Is_ATest") == ComponentName(["this", "is", "a", "test"]) + assert NameStyleBproto.fromStr("ThisIs-A_test") == ComponentName(["this", "is", "a", "test"]) + + +def test_ComponentNameStyleBproto_context_enum(): + assert NameStyleBproto.fromStr("test", "enum") == ComponentName(["test"]) + assert NameStyleBproto.fromStr("ThisIsATest", "enum") == ComponentName(["thisisatest"]) + assert NameStyleBproto.fromStr("TESTTEST", "enum") == ComponentName(["testtest"]) + assert NameStyleBproto.fromStr("TEST_TEST", "enum") == ComponentName(["test", "test"]) + + +def test_ComponentNameStyleBproto_comparison(): + c = NameStyleBproto.fromStr("ThisIsATest") + assert c == "this-is-a-test" + assert c == "this_is_a_test" + assert c == "thisIsATest" + assert c == "ThisIsATest" + assert c == "This_Is_A_Test" + + +def test_ComponentNameStyleBproto_from_Component(): + c = ComponentName(["this", "is", "a", "test"]) + c2 = NameStyleBproto.fromStr(c) + + assert c == c2 diff --git a/test/compiler/name_conversion/test_name_brand_applier.py b/test/compiler/name_conversion/test_name_brand_applier.py new file mode 100644 index 0000000..82613db --- /dev/null +++ b/test/compiler/name_conversion/test_name_brand_applier.py @@ -0,0 +1,40 @@ +import sys +sys.path.append('src/') + +from nameHandling.brand_applier import BrandApplier +from protocol_components.message import Message +from nameHandling.base import ComponentName + + +def test_name_applier(): + protocolNameApplier = BrandApplier("brand") + + name = ComponentName(["test"]) + name = protocolNameApplier.apply(name) + assert name == ComponentName(["brand", "test"]) + + name = ComponentName(["test", "test"]) + name = protocolNameApplier.apply(name) + assert name == ComponentName(["brand", "test", "test"]) + + msg = Message(ComponentName(["this", "is", "a", "test"]), 0) + msg: Message = protocolNameApplier.apply(msg) + assert msg.name == ComponentName(["brand", "this", "is", "a", "test"]) + + protocolNameApplier = BrandApplier(ComponentName(["happy", "brand"])) + + msg = Message(ComponentName(["this", "is", "a", "test"]), 0) + msg: Message = protocolNameApplier.apply(msg) + assert msg.name == ComponentName(["happy", "brand", "this", "is", "a", "test"]) + + try: + protocolNameApplier.apply(None) + assert False + except Exception as e: + assert isinstance(e, TypeError) + + try: + protocolNameApplier = BrandApplier(None) + assert False + except Exception as e: + assert isinstance(e, TypeError) diff --git a/test/compiler/name_conversion/test_name_c_style.py b/test/compiler/name_conversion/test_name_c_style.py new file mode 100644 index 0000000..e29c430 --- /dev/null +++ b/test/compiler/name_conversion/test_name_c_style.py @@ -0,0 +1,73 @@ +import sys +sys.path.append('src/') + +from nameHandling.base import ComponentName +from protocol_components.protocolFactory import FactoryProtocolDefition +from protocol_components.enumeration import FactoryEnumeration, Enumeration +from protocol_components.bitfields import FactoryBitfield, Bitfield +from protocol_components.dtypes import BprotoFieldBaseType +from protocol_components.field import FactoryField +from protocol_components.message import FactoryMessage, Message +from nameHandling.style.cNameStyle import NameStyleC + + +def test_style_c_fromStr(): + try: + NameStyleC.fromStr("") + assert False + except Exception as e: + assert isinstance(e, NotImplementedError) + + +def test_style_c_toStr_enum(): + assert NameStyleC.toStr(ComponentName(["my", "custom", "Values"]), "enum_name") == "MyCustomValues" + assert NameStyleC.toStr(ComponentName(["my", "custom", "enum", "value"]), "enum_item") == "MY_CUSTOM_ENUM_VALUE" + assert NameStyleC.toStr(ComponentName(["my", "custom", "class"]), "struct_name") == "MyCustomClass" + assert NameStyleC.toStr(ComponentName(["my", "custom", "class", "member"]), "struct_member") == "myCustomClassMember" + assert NameStyleC.toStr(ComponentName(["my", "custom", "class"]), "bitfield_name") == "MyCustomClass" + assert NameStyleC.toStr(ComponentName(["my", "custom", "class", "member"]), "bitfield_member") == "myCustomClassMember" + + +def test_style_c_preprocessor(): + protocol_factory = FactoryProtocolDefition() + + enum_factory = FactoryEnumeration() + enum_factory.add_value("first-Value", 1) + enum_factory.add_value("second-Important-Value", 2) + protocol_factory.add_enum(enum_factory.assemble("mode")) + + bitfield_factory = FactoryBitfield() + bitfield_factory.add_bit("FirstBit", 0) + bitfield_factory.add_bit("SecondBit", 1) + protocol_factory.add_bitfield(bitfield_factory.assemble("bitfield1")) + + message_factory = FactoryMessage() + message_factory.add_field(FactoryField().assemble("x", 1, BprotoFieldBaseType.FLOAT32, 2, None)) + message_factory.add_field(FactoryField().assemble("y", 2, BprotoFieldBaseType.FLOAT32, 2, None)) + protocol_factory.add_message(message_factory.assemble("cordUpdate", 1)) + + protocol = protocol_factory.assemble("ABCP", 5) + protocol = NameStyleC.preprocess(protocol) + + enum: Enumeration = list(protocol.enums.values())[0] + bitfield: Bitfield = list(protocol.bitfields.values())[0] + message: Message = list(protocol.messages.values())[0] + + assert isinstance(enum, Enumeration) + assert isinstance(bitfield, Bitfield) + assert isinstance(message, Message) + + assert NameStyleC.toStr(enum.name, "enum_name") == "Abcp_enum_Mode" + assert NameStyleC.toStr(bitfield.name, "bitfield_name") == "Abcp_bitfield_Bitfield1" + assert NameStyleC.toStr(message.name, "struct_name") == "Abcp_message_CordUpdate" + + assert enum.values == { + ComponentName(["abcp", "_enum_", "mode", "value", "first", "value"]): 1, + ComponentName(["abcp", "_enum_", "mode", "value", "second", "important", "value"]): 2 + } + + assert bitfield.bits == { + ComponentName(["first", "bit"]): 0, + ComponentName(["second", "bit"]): 1 + } + diff --git a/test/compiler/name_conversion/test_name_python_style.py b/test/compiler/name_conversion/test_name_python_style.py new file mode 100644 index 0000000..b520618 --- /dev/null +++ b/test/compiler/name_conversion/test_name_python_style.py @@ -0,0 +1,59 @@ +import sys +sys.path.append('src/') + +from nameHandling.base import ComponentName +from protocol_components.protocolFactory import FactoryProtocolDefition +from protocol_components.enumeration import FactoryEnumeration, Enumeration +from protocol_components.bitfields import FactoryBitfield, Bitfield +from protocol_components.dtypes import BprotoFieldBaseType +from protocol_components.field import FactoryField +from protocol_components.message import FactoryMessage, Message +from nameHandling.style.pythonNameStyle import NameStylePython + + +def test_style_python_fromStr(): + try: + NameStylePython.fromStr("") + assert False + except Exception as e: + assert isinstance(e, NotImplementedError) + + +def test_style_python_toStr_enum(): + assert NameStylePython.toStr(ComponentName(["my", "custom", "enum"]), "enum_name") == "MyCustomEnum" + assert NameStylePython.toStr(ComponentName(["my", "custom", "enum", "value"]), "enum_item") == "MY_CUSTOM_ENUM_VALUE" + assert NameStylePython.toStr(ComponentName(["my", "custom", "class"]), "class_name") == "MyCustomClass" + assert NameStylePython.toStr(ComponentName(["my", "custom", "class", "member"]), "class_member") == "my_custom_class_member" + + +def test_style_python_preprocessor(): + protocol_factory = FactoryProtocolDefition() + + enum_factory = FactoryEnumeration() + enum_factory.add_value("first-Value", 1) + enum_factory.add_value("second-Important-Value", 2) + protocol_factory.add_enum(enum_factory.assemble("mode")) + + bitfield_factory = FactoryBitfield() + bitfield_factory.add_bit("FirstBit", 0) + bitfield_factory.add_bit("SecondBit", 1) + protocol_factory.add_bitfield(bitfield_factory.assemble("bitfield1")) + + message_factory = FactoryMessage() + message_factory.add_field(FactoryField().assemble("x", 1, BprotoFieldBaseType.FLOAT32, 2, None)) + message_factory.add_field(FactoryField().assemble("y", 2, BprotoFieldBaseType.FLOAT32, 2, None)) + protocol_factory.add_message(message_factory.assemble("cordUpdate", 1)) + + protocol = protocol_factory.assemble("HCPT", 5) + protocol = NameStylePython.preprocess(protocol) + + enum: Enumeration = list(protocol.enums.values())[0] + bitfield: Bitfield = list(protocol.bitfields.values())[0] + message: Message = list(protocol.messages.values())[0] + + assert isinstance(enum, Enumeration) + assert isinstance(bitfield, Bitfield) + + assert NameStylePython.toStr(enum.name, "enum_name") == "HcptEnumMode" + assert NameStylePython.toStr(bitfield.name, "class_name") == "HcptBitfieldBitfield1" + assert NameStylePython.toStr(message.name, "class_name") == "HcptMessageCordUpdate" diff --git a/test/compiler/test_bitfields.py b/test/compiler/test_bitfields.py new file mode 100644 index 0000000..92bb9b8 --- /dev/null +++ b/test/compiler/test_bitfields.py @@ -0,0 +1,80 @@ +import sys +sys.path.append('src/') + +from nameHandling.base import ComponentName +from protocol_components.bitfields import FactoryBitfield, Bitfield +from errors import BprotoDuplicateNameError, BprotoDuplicateBitfieldPositionError, BprotoCompilerError +from nameHandling.base import NameStyleBproto + + +def test_bitfield_factory(): + fatory = FactoryBitfield() + fatory.add_bit("BIT #1", 0) + fatory.add_bit("BIT #4", None) + fatory.add_bit("BIT #2", 1) + fatory.add_bit("BIT #5", 4) + fatory.add_bit("BIT #6", None) + fatory.add_bit("BIT #3", 2) + fatory.add_bit("BIT #7", 6) + + bitfield: Bitfield = fatory.assemble("TestBitfield") + + assert bitfield.name == "TestBitfield" + assert isinstance(bitfield.name, ComponentName) + assert isinstance(bitfield.get_name(), ComponentName) + assert isinstance(bitfield.get_identifier(), ComponentName) + + assert bitfield.bits == { + NameStyleBproto.fromStr("BIT #1"): 0, + NameStyleBproto.fromStr("BIT #2"): 1, + NameStyleBproto.fromStr("BIT #3"): 2, + NameStyleBproto.fromStr("BIT #4"): 3, + NameStyleBproto.fromStr("BIT #5"): 4, + NameStyleBproto.fromStr("BIT #6"): 5, + NameStyleBproto.fromStr("BIT #7"): 6, + } + assert isinstance(list(bitfield.bits.keys())[0], ComponentName) + + +def test_bitfield_factory_failure_cases_duplicate_name(): + factory = FactoryBitfield() + + factory.add_bit("BIT #1", 0) + factory.add_bit("BIT #1", 1) + factory.add_bit("BIT #2", 2) + + try: + factory.assemble("TestBitfield") + assert False + except BprotoCompilerError as e: + assert isinstance(e, BprotoDuplicateNameError) + + +def test_bitfield_factory_failure_cases_duplicate_position(): + factory = FactoryBitfield() + + factory.add_bit("BIT #1", 0) + factory.add_bit("BIT #2", 1) + factory.add_bit("BIT #3", 1) + + try: + factory.assemble("TestBitfield") + assert False + except BprotoCompilerError as e: + assert isinstance(e, BprotoDuplicateBitfieldPositionError) + + +def test_bitfield_size_calculation(): + bitfield = Bitfield("TestBitfield") + + bitfield.length = 7 + assert bitfield.get_size_bytes() == 1 + + bitfield.length = 0 + assert bitfield.get_size_bytes() == 1 + + bitfield.length = 8 + assert bitfield.get_size_bytes() == 1 + + bitfield.length = 9 + assert bitfield.get_size_bytes() == 2 diff --git a/test/compiler/test_deep_copy.py b/test/compiler/test_deep_copy.py new file mode 100644 index 0000000..72385a7 --- /dev/null +++ b/test/compiler/test_deep_copy.py @@ -0,0 +1,73 @@ +from nameHandling.base import ComponentName, NameStyleBproto +from nameHandling.style.pythonNameStyle import NameStylePython +from protocol_components.enumeration import FactoryEnumeration +from copy import deepcopy + + +def test_deepcopy_component_name(): + name = ComponentName(["This", "Is", "Some", "text"], NameStylePython, source_string="ThisIsSomeText") + + new_name = deepcopy(name) + + name.name_parts = ["Some", "Other", "Stuff"] + name.source_string = "SomeOtherStuff" + name.source_style = NameStyleBproto + + # Verify Values did not change + assert name.name_parts != new_name.name_parts + assert name.source_string != new_name.source_string + assert name.source_style != new_name.source_style + + +def test_deepcopy_enum(): + old_enum_factory = FactoryEnumeration() + old_enum_factory.add_value("Value1", 1) + old_enum_factory.add_value("Value2", 2) + old_enum_factory.add_value("Value3", 3) + old_enum = old_enum_factory.assemble("OldEnum") + + new_enum = deepcopy(old_enum) + + # Check ids + assert id(new_enum) != id(old_enum) + assert id(new_enum.name) != id(old_enum.name) + + assert id(old_enum.values) != id(new_enum.values) + + old_keys = list(old_enum.values.keys()) + new_keys = list(new_enum.values.keys()) + assert id(old_keys[0]) != id(new_keys[0]) + assert id(old_keys[1]) != id(new_keys[1]) + assert id(old_keys[2]) != id(new_keys[2]) + + +def test_deepcopy_component_names_memo(): + name1 = NameStyleBproto.fromStr("Value1") + name2 = NameStyleBproto.fromStr("Value2") + + assert id(name1) != id(name2) + + name1_copy = deepcopy(name1) + assert isinstance(name1_copy, ComponentName) + + assert name1_copy == name1 + assert id(name1_copy) != id(name1) + + name1_copy2 = deepcopy(name1, { + id(name1): name1_copy, + id(name1_copy): name1_copy + }) + + name1_copy3 = deepcopy(name1_copy, { + id(name1): name1_copy, + id(name1_copy): name1_copy + }) + + assert isinstance(name1_copy2, ComponentName) + assert isinstance(name1_copy3, ComponentName) + + assert id(name1_copy) == id(name1_copy2) + assert id(name1_copy2) != id(name1) + + assert id(name1_copy) == id(name1_copy3) + assert id(name1_copy3) != id(name1) diff --git a/test/compiler/test_emums.py b/test/compiler/test_emums.py new file mode 100644 index 0000000..f0e7e24 --- /dev/null +++ b/test/compiler/test_emums.py @@ -0,0 +1,228 @@ +import sys +sys.path.append('src/') + +from protocol_components.dtypes import BprotoFieldBaseType +from protocol_components.enumeration import FactoryEnumeration, Enumeration +from errors import BprotoDuplicateNameError, BprotoDuplicateEnumValueError, BprotoCompilerError, BprotoEnumBitsizeTooLargeError +from nameHandling.base import NameStyleBproto, ComponentName + + +def test_enum_factory(): + + factory = FactoryEnumeration() + + factory.add_value("X", 0) + factory.add_value("A", 1) + factory.add_value("C", 3) + factory.add_value("B", 2) + factory.add_value("D", None) + factory.add_value("E", 5) + factory.add_value("F", None) + + enum = factory.assemble("TestEnum") + + assert enum.name == "TestEnum" + assert enum.values == { + NameStyleBproto.fromStr("X"): 0, + NameStyleBproto.fromStr("A"): 1, + NameStyleBproto.fromStr("B"): 2, + NameStyleBproto.fromStr("C"): 3, + NameStyleBproto.fromStr("D"): 4, + NameStyleBproto.fromStr("E"): 5, + NameStyleBproto.fromStr("F"): 6 + } + + assert isinstance(enum.name, ComponentName) + assert isinstance(enum.get_name(), ComponentName) + assert isinstance(enum.get_identifier(), ComponentName) + assert isinstance(list(enum.values.keys())[0], ComponentName) + + +def test_enum_factory_failure_cases_dubplicate_value(): + + factory = FactoryEnumeration() + + factory.add_value("X", 0) + factory.add_value("Y", 0) + factory.add_value("Z", 1) + + try: + factory.assemble("TestEnum") + assert False + except BprotoCompilerError as e: + assert isinstance(e, BprotoDuplicateEnumValueError) + + +def test_enum_factory_failure_cases_duplicate_name(): + factory = FactoryEnumeration() + + factory.add_value("X", 0) + factory.add_value("Y", 1) + factory.add_value("Y", 2) + + try: + factory.assemble("TestEnum") + assert False + except BprotoCompilerError as e: + assert isinstance(e, BprotoDuplicateNameError) + + +def test_enum_factory_failure_cases_duplicate_name_and_value(): + factory = FactoryEnumeration() + + factory.add_value("X", 0) + factory.add_value("X", 0) + + try: + factory.assemble("TestEnum") + assert False + except BprotoCompilerError as e: + assert isinstance(e, BprotoDuplicateNameError) or isinstance(e, BprotoDuplicateEnumValueError) + + +def test_enum_sorted(): + factory = FactoryEnumeration() + + factory.add_value("X", 0) + factory.add_value("D", None) + factory.add_value("C", 3) + factory.add_value("E", 5) + factory.add_value("F", None) + factory.add_value("B", 2) + factory.add_value("A", 1) + + enum = factory.assemble("TestEnum") + + assert list(enum.values.values()) == list(range(7)) + + +def test_enum_size_calc(): + assert FactoryEnumeration.calculate_bitsize(list(range(17))) == 5 + assert FactoryEnumeration.calculate_bitsize(list(range(16))) == 4 + assert FactoryEnumeration.calculate_bitsize(list(range(9))) == 4 + assert FactoryEnumeration.calculate_bitsize(list(range(8))) == 3 + assert FactoryEnumeration.calculate_bitsize(list(range(7))) == 3 + + assert FactoryEnumeration.calculate_bitsize([1, 2, 31]) == 5 + assert FactoryEnumeration.calculate_bitsize([1, 32, 2]) == 6 + assert FactoryEnumeration.calculate_bitsize([127, 0, 2]) == 7 + + assert FactoryEnumeration.calculate_bitsize([]) == 1 + + +def test_enum_auxiliary_data_typemap(): + + e = Enumeration("TestEnum", 8) + assert e.map_auxiliary_datatypes() == BprotoFieldBaseType.UINT8 + + e = Enumeration("TestEnum", 16) + assert e.map_auxiliary_datatypes() == BprotoFieldBaseType.UINT16 + + e = Enumeration("TestEnum", 24) + assert e.map_auxiliary_datatypes() == BprotoFieldBaseType.UINT32 + + e = Enumeration("TestEnum", 32) + assert e.map_auxiliary_datatypes() == BprotoFieldBaseType.UINT32 + + e = Enumeration("TestEnum", 64) + assert e.map_auxiliary_datatypes() == BprotoFieldBaseType.UINT64 + + e = Enumeration("TestEnum", 65) + try: + e.map_auxiliary_datatypes() + assert False + except Exception as err: + assert isinstance(err, BprotoEnumBitsizeTooLargeError) + + +def test_enum_size_calculation(): + # Uint8 + factory = FactoryEnumeration() + for i in range(3): + factory.add_value(f"#{i}", i) + e = factory.assemble("TestEnum") + assert e.get_size_bits() == 8 + assert e.get_size_bytes() == 1 + + factory = FactoryEnumeration() + for i in range(2**8): + factory.add_value(f"#{i}", i) + e = factory.assemble("TestEnum") + assert e.get_size_bits() == 8 + assert e.get_size_bytes() == 1 + + # Uint16 + factory = FactoryEnumeration() + for i in range(2**8 + 1): + factory.add_value(f"#{i}", i) + e = factory.assemble("TestEnum") + assert e.get_size_bits() == 16 + assert e.get_size_bytes() == 2 + + factory = FactoryEnumeration() + for i in range(2**16): + factory.add_value(f"#{i}", i) + e = factory.assemble("TestEnum") + assert e.get_size_bits() == 16 + assert e.get_size_bytes() == 2 + + # Uint32 + factory = FactoryEnumeration() + for i in range(17): + factory.add_value(f"#{i}", i) + factory.add_value(f"#{2**16}", 2**16) + # We start couinting from 0 -> 2**16 does not fit in 16 bits + + e = factory.assemble("TestEnum") + assert e.get_size_bits() == 32 + assert e.get_size_bytes() == 4 + + factory = FactoryEnumeration() + for i in range(17): + factory.add_value(f"#{i}", i) + factory.add_value(f"#{2**32 - 1}", 2**32 - 1) + # We start couinting from 0 2**32 - 1 does barely fit in 32 bits + + e = factory.assemble("TestEnum") + assert e.get_size_bits() == 32 + assert e.get_size_bytes() == 4 + + # Uint64 + factory = FactoryEnumeration() + for i in range(17): + factory.add_value(f"#{i}", i) + factory.add_value(f"#{2**32}", 2**32) + # We start couinting from 0 -> 2**32 does not fit in 32 bits + + e = factory.assemble("TestEnum") + assert e.get_size_bits() == 64 + assert e.get_size_bytes() == 8 + + factory = FactoryEnumeration() + for i in range(17): + factory.add_value(f"#{i}", i) + factory.add_value(f"#{2**64 - 1}", 2**64 - 1) + # We start couinting from 0 2**64 - 1 does barely fit in 64 bits + + e = factory.assemble("TestEnum") + assert e.get_size_bits() == 64 + assert e.get_size_bytes() == 8 + + # Too large + factory = FactoryEnumeration() + for i in range(17): + factory.add_value(f"#{i}", i) + factory.add_value(f"#{2**64}", 2**64) + + # This can cause rounding errors + # log2(2**64) ~ log2(2**64 - 1) ~ 64 + # This is btw not a problem for 2**32 + # stupid floating point errors + + # We start couinting from 0 -> 2**64 does not fit in 64 bits + + try: + e = factory.assemble("TestEnum") + assert False + except Exception as err: + assert isinstance(err, BprotoEnumBitsizeTooLargeError) diff --git a/test/compiler/test_field.py b/test/compiler/test_field.py new file mode 100644 index 0000000..757e165 --- /dev/null +++ b/test/compiler/test_field.py @@ -0,0 +1,98 @@ +import sys +sys.path.append('src/') + +from protocol_components.dtypes import BprotoFieldBaseType, BprotoArrayPolicyViolationError, BprotoUnknownDataTypeError +from protocol_components.field import Field, FactoryField, FieldBitfield, FieldBitfieldRef +from protocol_components.bitfields import Bitfield +from nameHandling.base import ComponentName + + +def test_field_object(): + field = Field("field1", 3, BprotoFieldBaseType.UINT64, 4) + + assert field.name == "field1" + assert field.pos == 3 + assert field.type == BprotoFieldBaseType.UINT64 + assert field.array_size == 4 + + assert field.get_identifier() == "field1" + assert field.get_name() == "field1" + assert field.get_type_name() == "field" + assert field.get_size_bytes() == 8 * 4 + assert field.get_size_bits() == 64 * 4 + + +def test_field_factory(): + factory = FactoryField() + + field: Field = factory.assemble("field1", 3, BprotoFieldBaseType.UINT64.value, 4, None) + + assert isinstance(field, Field) + assert field.name == "field1" + assert field.pos == 3 + assert field.type == BprotoFieldBaseType.UINT64 + assert field.array_size == 4 + + assert isinstance(field.name, ComponentName) + assert isinstance(field.get_identifier(), ComponentName) + assert isinstance(field.get_name(), ComponentName) + assert isinstance(field.get_type_name(), str) + + +def test_field_factory_errors_invalid_datatype(): + factory = FactoryField() + try: + factory.assemble( + "field1", 3, "junkDataType", 4, None + ) + assert False + except BprotoUnknownDataTypeError: + assert True + except Exception as e: + assert False, f"Unexpected exception: {e}" + + +def test_field_factory_errors_invalid_array_policy(): + factory = FactoryField() + try: + factory.assemble( + "field1", 1, BprotoFieldBaseType.BITFIELD, 4, None + ) + assert False + except BprotoArrayPolicyViolationError: + assert True + except Exception as e: + assert False, f"Unexpected exception: {e}" + + +def test_field_factory_bitfield(): + factory = FactoryField() + + field: FieldBitfield = factory.assemble( + "field1", 3, BprotoFieldBaseType.BITFIELD, 1, + Bitfield("test") + ) + + assert isinstance(field, FieldBitfield) + assert field.name == "field1" + assert field.pos == 3 + assert field.type == BprotoFieldBaseType.BITFIELD + assert field.array_size == 1 + assert isinstance(field.bitfield, Bitfield) + assert field.bitfield.get_name() == "test" + + +def test_field_factory_bitfield_ref(): + factory = FactoryField() + + field: FieldBitfield = factory.assemble( + "field1", 3, BprotoFieldBaseType.BITFIELD, 1, + "test" + ) + + assert isinstance(field, FieldBitfieldRef) + assert field.name == "field1" + assert field.pos == 3 + assert field.type == BprotoFieldBaseType.BITFIELD + assert field.array_size == 1 + assert field.ref == "test" diff --git a/test/compiler/test_message.py b/test/compiler/test_message.py new file mode 100644 index 0000000..1d9aea6 --- /dev/null +++ b/test/compiler/test_message.py @@ -0,0 +1,71 @@ +import sys +sys.path.append('src/') + +from nameHandling.base import ComponentName, NameStyleBproto +from protocol_components.message import FactoryMessage, Message +from protocol_components.field import FactoryField +from errors import BprotoDuplicateNameError, BprotoMessageIDAlreadyUsed + + +def test_message_factory(): + factory = FactoryMessage() + field1 = FactoryField().assemble("field1", 0, "uint64", 4, None) + field2 = FactoryField().assemble("field2", 1, "uint64", 4, None) + field3 = FactoryField().assemble("field3", 2, "uint64", 4, None) + field4 = FactoryField().assemble("field4", 3, "uint64", 4, None) + + factory.add_field(field3) + factory.add_field(field2) + factory.add_field(field4) + factory.add_field(field1) + + message: Message = factory.assemble("TestMessage", 42) + + assert message.name == "TestMessage" + assert isinstance(message.name, ComponentName) + assert isinstance(message.get_name(), ComponentName) + + assert message.message_index_number == 42 + assert message.get_identifier() == 42 + + assert message.fields == { + NameStyleBproto.fromStr("field1"): field1, + NameStyleBproto.fromStr("field2"): field2, + NameStyleBproto.fromStr("field3"): field3, + NameStyleBproto.fromStr("field4"): field4 + } + + assert isinstance(list(message.fields.keys())[0], ComponentName) + + assert message.get_size_bytes() == 8 * 4 * 4 + assert message.get_size_bits() == 64 * 4 * 4 + + +def test_message_factory_error_duplicate_name(): + field1 = FactoryField().assemble("field1", 0, "uint64", 4, None) + field2 = FactoryField().assemble("field1", 1, "uint64", 4, None) + + factory = FactoryMessage() + factory.add_field(field1) + factory.add_field(field2) + + try: + factory.assemble("TestMessage", 42) + assert False + except Exception as e: + assert isinstance(e, BprotoDuplicateNameError) + + +def test_message_factory_error_duplicate_index(): + field1 = FactoryField().assemble("field1", 0, "uint64", 4, None) + field2 = FactoryField().assemble("field2", 0, "uint64", 4, None) + + factory = FactoryMessage() + factory.add_field(field1) + factory.add_field(field2) + + try: + factory.assemble("TestMessage", 42) + assert False + except Exception as e: + assert isinstance(e, BprotoMessageIDAlreadyUsed) diff --git a/test/compiler/test_protocol.py b/test/compiler/test_protocol.py new file mode 100644 index 0000000..8b93f80 --- /dev/null +++ b/test/compiler/test_protocol.py @@ -0,0 +1,181 @@ +import sys +sys.path.append('src/') + +from protocol_components.dtypes import BprotoFieldBaseType +from nameHandling.base import ComponentName, NameStyleBproto +from protocol_components.message import FactoryMessage +from protocol_components.enumeration import FactoryEnumeration, Enumeration +from protocol_components.bitfields import FactoryBitfield, Bitfield +from protocol_components.protocolFactory import FactoryProtocolDefition +from protocol_components.field import FactoryField, Field, FieldEnum, FieldBitfield, FieldEnumRef, FieldBitfieldRef +from errors import BprotoUnresolvedReferenceError, BprotoAlreadyDefinedError + +from nameHandling.resolver import ProtocolReferenceResolver + + +def test_protocol_factory(): + factory = FactoryProtocolDefition() + factory.add_message(FactoryMessage().assemble("Message1", 42)) + factory.add_message(FactoryMessage().assemble("Message2", 43)) + + message_with_fields_factory = FactoryMessage() + message_with_fields_factory.add_field(FactoryField().assemble("field1", 0, "uint64", 1, None)) + message_with_fields_factory.add_field(FactoryField().assemble("enum_ref", 1, BprotoFieldBaseType.ENUM, 1, "Enum1")) + message_with_fields_factory.add_field(FactoryField().assemble("bitfield_ref", 2, BprotoFieldBaseType.BITFIELD, 1, "Bitfield2")) + + factory.add_message( + message_with_fields_factory.assemble("Message3", 44) + ) + + factory.add_enum(FactoryEnumeration().assemble("Enum1")) + factory.add_enum(FactoryEnumeration().assemble("Enum2")) + factory.add_enum(FactoryEnumeration().assemble("Enum3")) + + factory.add_bitfield(FactoryBitfield().assemble("Bitfield1")) + factory.add_bitfield(FactoryBitfield().assemble("Bitfield2")) + factory.add_bitfield(FactoryBitfield().assemble("Bitfield3")) + + protocol = factory.assemble("TESTPROTOCOL", 1) + + assert protocol.name.name_parts == ["testprotocol"] + assert isinstance(protocol.name, ComponentName) + assert protocol.version == 1 + + assert protocol.messages == { + NameStyleBproto.fromStr("Message1"): factory.messages.get(NameStyleBproto.fromStr("Message1")), + NameStyleBproto.fromStr("Message2"): factory.messages.get(NameStyleBproto.fromStr("Message2")), + NameStyleBproto.fromStr("Message3"): factory.messages.get(NameStyleBproto.fromStr("Message3")) + } + + assert protocol.enums == { + NameStyleBproto.fromStr("Enum1"): factory.enums.get(NameStyleBproto.fromStr("Enum1")), + NameStyleBproto.fromStr("Enum2"): factory.enums.get(NameStyleBproto.fromStr("Enum2")), + NameStyleBproto.fromStr("Enum3"): factory.enums.get(NameStyleBproto.fromStr("Enum3")) + } + + assert protocol.bitfields == { + NameStyleBproto.fromStr("Bitfield1"): factory.bitfields.get(NameStyleBproto.fromStr("Bitfield1")), + NameStyleBproto.fromStr("Bitfield2"): factory.bitfields.get(NameStyleBproto.fromStr("Bitfield2")), + NameStyleBproto.fromStr("Bitfield3"): factory.bitfields.get(NameStyleBproto.fromStr("Bitfield3")) + } + + # Check if references are resolved works + assert isinstance(protocol.messages.get(NameStyleBproto.fromStr("Message3")).fields.get(NameStyleBproto.fromStr("enum_ref")), FieldEnum) + assert isinstance(protocol.messages.get(NameStyleBproto.fromStr("Message3")).fields.get(NameStyleBproto.fromStr("bitfield_ref")), FieldBitfield) + + +def test_protocol_resolve_refernces(): + enum1 = FactoryEnumeration().assemble("Enum1") + bitfield1 = FactoryBitfield().assemble("Bitfield1") + + message1Factory = FactoryMessage() + message1Factory.add_field(FactoryField().assemble("field1", 0, "uint64", 1, None)) + message1Factory.add_field(FactoryField().assemble("enum_field", 1, BprotoFieldBaseType.ENUM, 1, enum1)) + message1Factory.add_field(FactoryField().assemble("enum_ref_field", 2, BprotoFieldBaseType.ENUM, 1, "Enum1")) + message1Factory.add_field(FactoryField().assemble("bitfield_field", 3, BprotoFieldBaseType.BITFIELD, 1, bitfield1)) + message1Factory.add_field(FactoryField().assemble("bitfield_ref_field", 4, BprotoFieldBaseType.BITFIELD, 1, "Bitfield1")) + message1 = message1Factory.assemble("Message1", 42) + + # Check if message is correctly assembled + assert isinstance(message1.fields.get(NameStyleBproto.fromStr("field1")), Field) + assert message1.fields.get(NameStyleBproto.fromStr("field1")).type == BprotoFieldBaseType.UINT64 + assert message1.fields.get(NameStyleBproto.fromStr("field1")).array_size == 1 + assert message1.fields.get(NameStyleBproto.fromStr("field1")).ref is None + assert isinstance(message1.fields.get(NameStyleBproto.fromStr("field1")).ref, type(None)) + + assert isinstance(message1.fields.get(NameStyleBproto.fromStr("enum_field")), FieldEnum) + assert message1.fields.get(NameStyleBproto.fromStr("enum_field")).type == BprotoFieldBaseType.ENUM + assert isinstance(message1.fields.get(NameStyleBproto.fromStr("enum_field")).enum, Enumeration) + assert message1.fields.get(NameStyleBproto.fromStr("enum_field")).enum == enum1 + + assert isinstance(message1.fields.get(NameStyleBproto.fromStr("enum_ref_field")), FieldEnumRef) + assert message1.fields.get(NameStyleBproto.fromStr("enum_ref_field")).type == BprotoFieldBaseType.ENUM + assert message1.fields.get(NameStyleBproto.fromStr("enum_ref_field")).ref == "Enum1" + + assert isinstance(message1.fields.get(NameStyleBproto.fromStr("bitfield_field")), FieldBitfield) + assert message1.fields.get(NameStyleBproto.fromStr("bitfield_field")).type == BprotoFieldBaseType.BITFIELD + assert isinstance(message1.fields.get(NameStyleBproto.fromStr("bitfield_field")).bitfield, Bitfield) + assert message1.fields.get(NameStyleBproto.fromStr("bitfield_field")).bitfield == bitfield1 + + assert isinstance(message1.fields.get(NameStyleBproto.fromStr("bitfield_ref_field")), FieldBitfieldRef) + assert message1.fields.get(NameStyleBproto.fromStr("bitfield_ref_field")).type == BprotoFieldBaseType.BITFIELD + assert message1.fields.get(NameStyleBproto.fromStr("bitfield_ref_field")).ref == "Bitfield1" + + factory = FactoryProtocolDefition() + factory.add_message(message1) + factory.add_enum(enum1) + factory.add_bitfield(bitfield1) + protocol = factory.assemble("TestProtocol", 1) + + # Check if references are resolved works + + resolver = ProtocolReferenceResolver(protocol) + resolver.resolve_refrence_protocol() + + assert protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("enum_field")).type == BprotoFieldBaseType.ENUM + assert isinstance(protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("enum_field")), FieldEnum) + assert protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("enum_field")).enum == enum1 + + assert protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("enum_ref_field")).type == BprotoFieldBaseType.ENUM + assert isinstance(protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("enum_ref_field")), FieldEnum) + assert protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("enum_ref_field")).enum == enum1 + + assert protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("bitfield_field")).type == BprotoFieldBaseType.BITFIELD + assert isinstance(protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("bitfield_field")), FieldBitfield) + assert protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("bitfield_field")).bitfield == bitfield1 + + assert protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("bitfield_ref_field")).type == BprotoFieldBaseType.BITFIELD + assert isinstance(protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("bitfield_ref_field")), FieldBitfield) + assert protocol.messages.get(NameStyleBproto.fromStr("Message1")).fields.get(NameStyleBproto.fromStr("bitfield_ref_field")).bitfield == bitfield1 + + +def test_protocol_resolve_refernces_unknown_refernces(): + message1Factory = FactoryMessage() + message1Factory.add_field(FactoryField().assemble("field1", 0, "uint64", 1, None)) + message1Factory.add_field(FactoryField().assemble("enum_ref_field", 1, BprotoFieldBaseType.ENUM, 1, "Enum1")) + message1Factory.add_field(FactoryField().assemble("bitfield_ref_field", 2, BprotoFieldBaseType.BITFIELD, 1, "Bitfield1",)) + message1 = message1Factory.assemble("Message1", 42) + + factory = FactoryProtocolDefition() + factory.add_message(message1) + + try: + factory.assemble("TestProtocol", 1) + assert False + except Exception as e: + assert isinstance(e, BprotoUnresolvedReferenceError) + + try: + factory.assemble("TestProtocol", 1) + assert False + except Exception as e: + assert isinstance(e, BprotoUnresolvedReferenceError) + + +def test_protocol_factory_already_define_error(): + factory = FactoryProtocolDefition() + factory.add_message(FactoryMessage().assemble("Message1", 42)) + try: + factory.add_message(FactoryMessage().assemble("Message1", 42)) + factory.assemble("A", 1) + assert False + except Exception as e: + assert isinstance(e, BprotoAlreadyDefinedError) + + factory = FactoryProtocolDefition() + factory.add_enum(FactoryEnumeration().assemble("Enum1")) + try: + factory.assemble("A", 1) + factory.add_enum(FactoryEnumeration().assemble("Enum1")) + assert False + except Exception as e: + assert isinstance(e, BprotoAlreadyDefinedError) + + factory = FactoryProtocolDefition() + factory.add_bitfield(FactoryBitfield().assemble("Bitfield1")) + try: + factory.add_bitfield(FactoryBitfield().assemble("Bitfield1")) + factory.assemble("A", 1) + assert False + except Exception as e: + assert isinstance(e, BprotoAlreadyDefinedError) diff --git a/test/data/static_content1.txt b/test/data/static_content1.txt new file mode 100644 index 0000000..f079749 --- /dev/null +++ b/test/data/static_content1.txt @@ -0,0 +1 @@ +test1 \ No newline at end of file diff --git a/test/data/static_folder/static_content2.txt b/test/data/static_folder/static_content2.txt new file mode 100644 index 0000000..d606037 --- /dev/null +++ b/test/data/static_folder/static_content2.txt @@ -0,0 +1 @@ +test2 \ No newline at end of file diff --git a/test/data/templates/template.jinja2 b/test/data/templates/template.jinja2 new file mode 100644 index 0000000..c7a43bc --- /dev/null +++ b/test/data/templates/template.jinja2 @@ -0,0 +1 @@ +Hello {{ name }}! \ No newline at end of file diff --git a/test/output/c/CMakeLists.txt b/test/output/c/CMakeLists.txt new file mode 100644 index 0000000..854f8cd --- /dev/null +++ b/test/output/c/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.25) +project(bproto_test) +enable_testing() + +include("../../backend_output/c/HCP.cmake") + +# include CTest support +include(CTest) +set(CMAKE_BUILD_TYPE "Debug") + +# Adding Unity test framework +add_library(unity STATIC Unity/src/unity.c) +target_include_directories(unity PUBLIC Unity/src) + +# Normal test target +add_executable(test_runner + src/main.c + src/test_messages.c +) + +target_link_libraries(test_runner PUBLIC + bproto + unity + gcov +) + +target_include_directories(test_runner PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/src +) + +target_compile_options(test_runner PRIVATE -fprofile-arcs -ftest-coverage -fPIC -O0 --coverage) +target_compile_options(bproto PRIVATE -fprofile-arcs -ftest-coverage -fPIC -O0 --coverage) +target_link_options(test_runner PRIVATE -fprofile-arcs -ftest-coverage -fPIC --coverage) +target_link_options(bproto PRIVATE -fprofile-arcs -ftest-coverage -fPIC --coverage) + +# Python Integration test build +add_executable(test_integration_python + src/test_python_integration.c +) + +target_link_libraries(test_integration_python PUBLIC + bproto + gcov +) + +target_include_directories(test_integration_python PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/src +) \ No newline at end of file diff --git a/test/output/c/src/Unity b/test/output/c/src/Unity new file mode 160000 index 0000000..23e8edb --- /dev/null +++ b/test/output/c/src/Unity @@ -0,0 +1 @@ +Subproject commit 23e8edbd643be2131a52254d366296b44b930d9a diff --git a/test/output/c/src/main.c b/test/output/c/src/main.c new file mode 100644 index 0000000..9251444 --- /dev/null +++ b/test/output/c/src/main.c @@ -0,0 +1,17 @@ +#include "unity.h" +#include "suit.h" + +void setUp(void) {}; +void tearDown(void) {}; + +int main() { + UNITY_BEGIN(); + RUN_TEST(test_encdec_generic_Error); + RUN_TEST(test_encdec_generic_MotionUpdate); + + RUN_TEST(test_encdec_generic_Error_invalid_id); + RUN_TEST(test_encdec_generic_Error_invalid_size); + RUN_TEST(test_encdec_generic_Error_large_size); + RUN_TEST(test_encdec_generic_Error_invalid_crc); + return UNITY_END(); +} \ No newline at end of file diff --git a/test/output/c/src/suit.h b/test/output/c/src/suit.h new file mode 100644 index 0000000..f11d271 --- /dev/null +++ b/test/output/c/src/suit.h @@ -0,0 +1,9 @@ +#pragma once + +// Enc/Dec with generic data buffer +void test_encdec_generic_MotionUpdate(); +void test_encdec_generic_Error(); +void test_encdec_generic_Error_invalid_id(); +void test_encdec_generic_Error_invalid_size(); +void test_encdec_generic_Error_large_size(); +void test_encdec_generic_Error_invalid_crc(); \ No newline at end of file diff --git a/test/output/c/src/test_messages.c b/test/output/c/src/test_messages.c new file mode 100644 index 0000000..50ebf55 --- /dev/null +++ b/test/output/c/src/test_messages.c @@ -0,0 +1,224 @@ +#include "suit.h" +#include +#include "unity.h" +#include "HCP_message.h" + +void test_encdec_generic_Error() { + uint8_t buffer_msg_data[MAX_MESSAGE_DATA_SIZE]; + + Hcp_message_MotionUpdate msg = { + .speed = {3, 6, 9}, + .stearing = 0.5, + .name = "This is a test", + .heading = 'B', + .enable = {true, false, true, false}, + .enables = { + .a = false, .b = true, .c = false + }, + .dangerLvl = HCP_ENUM_INLINE_MOTION_UPDATE_DANGER_LVL_VALUE_MELTDOWN + }; + + size_t length = toBytes_Hcp_message_MotionUpdate(&msg, buffer_msg_data); + + TEST_ASSERT_EQUAL(length, 58); + TEST_ASSERT_EQUAL(buffer_msg_data[0], 0x01); + + Hcp_message_MotionUpdate rx_msg; + size_t parsed_length = 0; + HCP_MSG_ID msg_id = 0xff; + HCP_PARSER_RESULT result = fromBytes(&rx_msg, buffer_msg_data, length, &parsed_length, &msg_id); + + TEST_ASSERT_EQUAL(result, HCP_PARSER_RESULT_OK); + TEST_ASSERT_EQUAL(parsed_length, 58); + TEST_ASSERT_EQUAL(msg_id, HCP_MESSAGE_MOTION_UPDATE_ID); + + TEST_ASSERT_EQUAL(msg.stearing, rx_msg.stearing); + TEST_ASSERT_EQUAL_STRING(msg.name, rx_msg.name); + TEST_ASSERT_EQUAL(msg.heading, rx_msg.heading); + TEST_ASSERT_EQUAL(msg.dangerLvl, rx_msg.dangerLvl); + + TEST_ASSERT_EQUAL(msg.speed[0], rx_msg.speed[0]); + TEST_ASSERT_EQUAL(msg.speed[1], rx_msg.speed[1]); + TEST_ASSERT_EQUAL(msg.speed[2], rx_msg.speed[2]); + + TEST_ASSERT_EQUAL(msg.enable[0], rx_msg.enable[0]); + TEST_ASSERT_EQUAL(msg.enable[1], rx_msg.enable[1]); + TEST_ASSERT_EQUAL(msg.enable[2], rx_msg.enable[2]); + TEST_ASSERT_EQUAL(msg.enable[3], rx_msg.enable[3]); + + TEST_ASSERT_EQUAL(msg.enables.a, rx_msg.enables.a); + TEST_ASSERT_EQUAL(msg.enables.b, rx_msg.enables.b); + TEST_ASSERT_EQUAL(msg.enables.c, rx_msg.enables.c); +} + +void test_encdec_generic_MotionUpdate() { + uint8_t buffer_msg_data[MAX_MESSAGE_DATA_SIZE]; + + Hcp_message_Error msg = { + .enables = { + .aa = true, + .asd = false, + .b = true, + .c = false, + .test = true + }, + .recoveryStatus = HCP_ENUM_INLINE_ERROR_RECOVERY_STATUS_VALUE_NO + }; + + size_t length = toBytes_Hcp_message_Error(&msg, buffer_msg_data); + + TEST_ASSERT_EQUAL(length, 5); + TEST_ASSERT_EQUAL(buffer_msg_data[0], 0x02); + + Hcp_message_Error rx_msg; + size_t parsed_length = 0; + HCP_MSG_ID msg_id = 0xff; + HCP_PARSER_RESULT result = fromBytes(&rx_msg, buffer_msg_data, length, &parsed_length, &msg_id); + + TEST_ASSERT_EQUAL(result, HCP_PARSER_RESULT_OK); + TEST_ASSERT_EQUAL(parsed_length, 5); + TEST_ASSERT_EQUAL(msg_id, HCP_MESSAGE_ERROR_ID); + + TEST_ASSERT_EQUAL(rx_msg.enables.aa, msg.enables.aa); + TEST_ASSERT_EQUAL(rx_msg.enables.asd, msg.enables.asd); + TEST_ASSERT_EQUAL(rx_msg.enables.b, msg.enables.b); + TEST_ASSERT_EQUAL(rx_msg.enables.c, msg.enables.c); + TEST_ASSERT_EQUAL(rx_msg.enables.test, msg.enables.test); + + TEST_ASSERT_EQUAL(rx_msg.recoveryStatus, msg.recoveryStatus); +} + +void test_encdec_generic_Error_invalid_id() { + uint8_t buffer_msg_data[MAX_MESSAGE_DATA_SIZE]; + + Hcp_message_Error msg = { + .enables = { + .aa = true, + .asd = false, + .b = true, + .c = false, + .test = true + }, + .recoveryStatus = HCP_ENUM_INLINE_ERROR_RECOVERY_STATUS_VALUE_NO + }; + + size_t length = toBytes_Hcp_message_Error(&msg, buffer_msg_data); + + TEST_ASSERT_EQUAL(length, 5); + TEST_ASSERT_EQUAL(buffer_msg_data[0], 0x02); + + // Mofiy data + buffer_msg_data[0] = 0xf2; + + Hcp_message_Error rx_msg; + size_t parsed_length = 0; + HCP_MSG_ID msg_id = 0xff; + HCP_PARSER_RESULT result = fromBytes(&rx_msg, buffer_msg_data, length, &parsed_length, &msg_id); + + TEST_ASSERT_EQUAL(parsed_length, 0); + TEST_ASSERT_EQUAL(result, HCP_PARSER_RESULT_ERROR_INVALID_ID); + TEST_ASSERT_EQUAL(msg_id, 0xff); +} + +void test_encdec_generic_Error_invalid_size() { + uint8_t buffer_msg_data[MAX_MESSAGE_DATA_SIZE]; + + Hcp_message_Error msg = { + .enables = { + .aa = true, + .asd = false, + .b = true, + .c = false, + .test = true + }, + .recoveryStatus = HCP_ENUM_INLINE_ERROR_RECOVERY_STATUS_VALUE_NO + }; + + size_t length = toBytes_Hcp_message_Error(&msg, buffer_msg_data); + + TEST_ASSERT_EQUAL(length, 5); + TEST_ASSERT_EQUAL(buffer_msg_data[0], 0x02); + + // Mofiy data + length = 2; + + Hcp_message_Error rx_msg; + size_t parsed_length = 0; + HCP_MSG_ID msg_id = 0xff; + HCP_PARSER_RESULT result = fromBytes(&rx_msg, buffer_msg_data, length, &parsed_length, &msg_id); + + TEST_ASSERT_EQUAL(parsed_length, 0); + TEST_ASSERT_EQUAL(result, HCP_PARSER_RESULT_ERROR_INVALID_SIZE); + TEST_ASSERT_EQUAL(msg_id, 0xff); +} + +void test_encdec_generic_Error_large_size() { + uint8_t buffer_msg_data[MAX_MESSAGE_DATA_SIZE]; + + Hcp_message_Error msg = { + .enables = { + .aa = true, + .asd = false, + .b = true, + .c = false, + .test = true + }, + .recoveryStatus = HCP_ENUM_INLINE_ERROR_RECOVERY_STATUS_VALUE_NO + }; + + size_t length = toBytes_Hcp_message_Error(&msg, buffer_msg_data); + + TEST_ASSERT_EQUAL(length, 5); + TEST_ASSERT_EQUAL(buffer_msg_data[0], 0x02); + + length += 5; + + Hcp_message_Error rx_msg; + size_t parsed_length = 0; + HCP_MSG_ID msg_id = 0xff; + HCP_PARSER_RESULT result = fromBytes(&rx_msg, buffer_msg_data, length, &parsed_length, &msg_id); + + TEST_ASSERT_EQUAL(result, HCP_PARSER_RESULT_OK); + TEST_ASSERT_EQUAL(parsed_length, 5); + TEST_ASSERT_EQUAL(msg_id, HCP_MESSAGE_ERROR_ID); + + TEST_ASSERT_EQUAL(rx_msg.enables.aa, msg.enables.aa); + TEST_ASSERT_EQUAL(rx_msg.enables.asd, msg.enables.asd); + TEST_ASSERT_EQUAL(rx_msg.enables.b, msg.enables.b); + TEST_ASSERT_EQUAL(rx_msg.enables.c, msg.enables.c); + TEST_ASSERT_EQUAL(rx_msg.enables.test, msg.enables.test); + + TEST_ASSERT_EQUAL(rx_msg.recoveryStatus, msg.recoveryStatus); +} + +void test_encdec_generic_Error_invalid_crc() { + uint8_t buffer_msg_data[MAX_MESSAGE_DATA_SIZE]; + + Hcp_message_Error msg = { + .enables = { + .aa = true, + .asd = false, + .b = true, + .c = false, + .test = true + }, + .recoveryStatus = HCP_ENUM_INLINE_ERROR_RECOVERY_STATUS_VALUE_NO + }; + + size_t length = toBytes_Hcp_message_Error(&msg, buffer_msg_data); + + TEST_ASSERT_EQUAL(length, 5); + TEST_ASSERT_EQUAL(buffer_msg_data[0], 0x02); + + // Mofiy data + buffer_msg_data[3] = 0xff; + + Hcp_message_Error rx_msg; + size_t parsed_length = 0; + HCP_MSG_ID msg_id = 0xff; + HCP_PARSER_RESULT result = fromBytes(&rx_msg, buffer_msg_data, length, &parsed_length, &msg_id); + + TEST_ASSERT_EQUAL(parsed_length, 0); + TEST_ASSERT_EQUAL(result, HCP_PARSER_RESULT_ERROR_INVALID_CRC); + TEST_ASSERT_EQUAL(msg_id, 0xff); +} diff --git a/test/output/c/src/test_python_integration.c b/test/output/c/src/test_python_integration.c new file mode 100644 index 0000000..1f2fb13 --- /dev/null +++ b/test/output/c/src/test_python_integration.c @@ -0,0 +1,84 @@ +#include +#include +#include +#include + +#include "HCP_message.h" +#include "HCP_enum.h" + +int main() { + char hex_string[1024]; + uint8_t data[sizeof(hex_string)/2]; + + size_t bytesRead = read(STDIN_FILENO, hex_string, sizeof(hex_string)); + + for(int i=0; i<(bytesRead/2); i+=1) { + sscanf(&hex_string[i*2], "%02hhx", &data[i]); + } + + // Decode the package + char package_buffer[MAX_MESSAGE_STRUCT_SIZE]; + + size_t parsed_size = 0; + HCP_MSG_ID msg_id; + HCP_PARSER_RESULT result = fromBytes( + (void *)(package_buffer), data, bytesRead/2, &parsed_size, &msg_id + ); + + // Early exit if the decode failed + if(result != HCP_PARSER_RESULT_OK) { + if(result == HCP_PARSER_RESULT_ERROR_INVALID_ID) { + printf("ID\n"); + return -1; + } else if(result == HCP_PARSER_RESULT_ERROR_INVALID_CRC) { + printf("CRC\n"); + return -1; + } else if(result == HCP_PARSER_RESULT_ERROR_INVALID_SIZE) { + printf("SIZE\n"); + return -1; + } else { + printf("???\n"); + return -1; + } + } + + // Modify and encode the package + char send_buffer[MAX_MESSAGE_DATA_SIZE]; + size_t send_size; + + switch (msg_id) { + case HCP_MESSAGE_MOTION_UPDATE_ID: { + Hcp_message_MotionUpdate *msg = (struct Hcp_message_MotionUpdate *)package_buffer; + msg->speed[0] += 1; + msg->speed[1] += 2; + msg->speed[2] += 3; + msg->stearing *= 0.5; + msg->name[0] = 'A'; + msg->enables.a = !msg->enables.a; + msg->enables.b = !msg->enables.b; + msg->enables.c = !msg->enables.c; + msg->dangerLvl = HCP_ENUM_INLINE_MOTION_UPDATE_DANGER_LVL_VALUE_MELTDOWN; + send_size = toBytes_Hcp_message_MotionUpdate(msg, send_buffer); + break; + } + case HCP_MESSAGE_ERROR_ID: { + Hcp_message_Error *msg = (struct Hcp_message_Error *)package_buffer; + msg->enables.b = !msg->enables.b; + msg->enables.c = !msg->enables.c; + send_size = toBytes_Hcp_message_Error(msg, send_buffer); + break; + } + default: + break; + } + + // Print the encoded package + printf("OK\n"); + for(int i=0; i tuple[int, str, bytes]: + proc = subprocess.Popen( + ["build/test_integration_python"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE + ) + stdout, _ = proc.communicate(data.hex().encode() + b"\n") + return_code = proc.wait() + + lines = stdout.splitlines() + + if return_code != 0: + return (return_code, lines[0].decode("ascii"), b"") + else: + return ( + return_code, + lines[0].decode("ascii"), + bytes.fromhex(lines[1].decode()) + ) + + +def test_integration_c_encdec_motion_update(): + from backend_output.python.HCP_protocol_packets import HcpMessageMotionUpdate + from backend_output.python.HCP_protocol_bitfield import HcpBitfieldEnables + from backend_output.python.HCP_protocol_enum import HcpEnumInlineMotionUpdateDangerLvl + + pkg = HcpMessageMotionUpdate( + speed=[1, 2, 3], + stearing=-0.1, + name="This is a test", + enable=[True, False, True, False], + heading="S", + enables=HcpBitfieldEnables( + a=True, b=False, c=True + ), + danger_lvl=HcpEnumInlineMotionUpdateDangerLvl.WARNING + ) + + # Send to Integration Test Binary + ret_code, _, data_incoming = send_c_binary(pkg.to_bytes()) + + assert ret_code == 0 + + # Decode the incoming data. + pkg_incoming = HcpMessageMotionUpdate().from_bytes(data_incoming) + + # Check that the incoming data is correctly modified. + assert pkg.speed[0] + 1 == pkg_incoming.speed[0] + assert pkg.speed[1] + 2 == pkg_incoming.speed[1] + assert pkg.speed[2] + 3 == pkg_incoming.speed[2] + + assert round(pkg.stearing * 0.5 * 1000) == round(pkg_incoming.stearing * 1000) + + assert pkg_incoming.name[0] == "A" + assert pkg.name[1:] == pkg_incoming.name[1:] + + assert pkg.enables.a != pkg_incoming.enables.a + assert pkg.enables.b != pkg_incoming.enables.b + assert pkg.enables.c != pkg_incoming.enables.c + + assert pkg.enable == pkg_incoming.enable + + assert pkg_incoming.danger_lvl.value == HcpEnumInlineMotionUpdateDangerLvl.MELTDOWN.value + + +def test_integration_c_encdec_error(): + from backend_output.python.HCP_protocol_packets import HcpMessageError + from backend_output.python.HCP_protocol_bitfield import HcpBitfieldInlineErrorEnables + from backend_output.python.HCP_protocol_enum import HcpEnumInlineErrorRecoveryStatus + + pkg = HcpMessageError( + recovery_status=HcpEnumInlineErrorRecoveryStatus.NO, + enables=HcpBitfieldInlineErrorEnables( + test=True, asd=False, b=False, c=True, aa=True + ), + ) + + # Send to Integration Test Binary + ret_code, _, data_incoming = send_c_binary(pkg.to_bytes()) + + assert ret_code == 0 + + # Decode the incoming data. + pkg_incoming = HcpMessageError().from_bytes(data_incoming) + + # Check that the incoming data is correctly modified. + assert pkg_incoming.recovery_status.value == pkg.recovery_status.value + assert pkg_incoming.enables.test == pkg.enables.test + assert pkg_incoming.enables.asd == pkg.enables.asd + assert pkg_incoming.enables.b != pkg.enables.b + assert pkg_incoming.enables.c != pkg.enables.c + assert pkg_incoming.enables.aa == pkg.enables.aa + + +def test_integration_c_encdec_failure_size(): + from backend_output.python.HCP_protocol_packets import HcpMessageError + from backend_output.python.HCP_protocol_bitfield import HcpBitfieldInlineErrorEnables + from backend_output.python.HCP_protocol_enum import HcpEnumInlineErrorRecoveryStatus + + pkg = HcpMessageError( + recovery_status=HcpEnumInlineErrorRecoveryStatus.NO, + enables=HcpBitfieldInlineErrorEnables( + test=True, asd=False, b=False, c=True, aa=True + ), + ) + + # Send to Integration Test Binary + data = pkg.to_bytes() + ret_code, msg, _ = send_c_binary(data[:4]) + + assert ret_code != 0 + assert msg == "SIZE" + + +def test_integration_c_encdec_failure_crc(): + from backend_output.python.HCP_protocol_packets import HcpMessageError + from backend_output.python.HCP_protocol_bitfield import HcpBitfieldInlineErrorEnables + from backend_output.python.HCP_protocol_enum import HcpEnumInlineErrorRecoveryStatus + + pkg = HcpMessageError( + recovery_status=HcpEnumInlineErrorRecoveryStatus.NO, + enables=HcpBitfieldInlineErrorEnables( + test=True, asd=False, b=False, c=True, aa=True + ), + ) + + # Send to Integration Test Binary + data = pkg.to_bytes() + data = data[:2] + b"\xaa" + data[3:] + ret_code, msg, _ = send_c_binary(data) + + assert ret_code != 0 + assert msg == "CRC" + + +def test_integration_c_encdec_failure_id(): + from backend_output.python.HCP_protocol_packets import HcpMessageError + from backend_output.python.HCP_protocol_bitfield import HcpBitfieldInlineErrorEnables + from backend_output.python.HCP_protocol_enum import HcpEnumInlineErrorRecoveryStatus + + pkg = HcpMessageError( + recovery_status=HcpEnumInlineErrorRecoveryStatus.NO, + enables=HcpBitfieldInlineErrorEnables( + test=True, asd=False, b=False, c=True, aa=True + ), + ) + + # Send to Integration Test Binary + data = pkg.to_bytes() + data = b"\xff" + data[1:] + ret_code, msg, _ = send_c_binary(data) + + assert ret_code != 0 + assert msg == "ID" diff --git a/test/output/test_crc.py b/test/output/test_crc.py new file mode 100644 index 0000000..acf71a7 --- /dev/null +++ b/test/output/test_crc.py @@ -0,0 +1,63 @@ + +def crc16(data: bytearray, offset: int, length: int): + if data is None or offset < 0 or offset > len(data) - 1 and offset + length > len(data): + return 0 + crc = 0xFFFF + for i in range(0, length): + crc ^= data[offset + i] << 8 + for j in range(0, 8): + if (crc & 0x8000) > 0: + crc = (crc << 1) ^ 0x1021 + else: + crc = crc << 1 + return crc & 0xFFFF + + +def test_crc_sum(): + # ignore: F401 + from backend_output.python.bproto_base import crc16_calc_inital, calc_crc_sum + + protocol_hash = bytes.fromhex("4a07789a41ef64c38a7edb78574497d4") + protocol_data = bytes.fromhex("c9850ff561ea6d27f3171e1c680a4607ea8fa5278ee96de5c8e0ade101bdeb4332d07cb2269a47c910ca2cfe95de9ceda0bb9ee05b0909dcaf68f6a605df7dd4") + + inital = crc16_calc_inital(protocol_hash) + checksum = calc_crc_sum(protocol_data, inital) + ref_data = protocol_hash + protocol_data + assert crc16(ref_data, 0, len(ref_data)) == checksum + + protocol_hash = bytes.fromhex("4a07789a41ef64c38a7edb78574497d4") + protocol_data = bytes.fromhex("8898ebbd28e82663b71a7bbf8bc5ec72872ca057a77e31c1c11deff5004e8985767acb75da9b2d1dea643b08ed0458477e4a") + + inital = crc16_calc_inital(protocol_hash) + checksum = calc_crc_sum(protocol_data, inital) + ref_data = protocol_hash + protocol_data + assert crc16(ref_data, 0, len(ref_data)) == checksum + + protocol_hash = bytes.fromhex("4a07789a41ef64c38a7edb78574497d4") + protocol_data = bytes.fromhex("ca321a923e202614482cb392d858175ffe9f117d38") + + inital = crc16_calc_inital(protocol_hash) + checksum = calc_crc_sum(protocol_data, inital) + ref_data = protocol_hash + protocol_data + assert crc16(ref_data, 0, len(ref_data)) == checksum + + +def test_crc_sum_change(): + # ignore: F401 + from backend_output.python.bproto_base import crc16_calc_inital, calc_crc_sum + + protocol_hash = bytes.fromhex("4a07789a41ef64c38a7edb78574497d4") + protocol_data = bytes.fromhex("ca321a923e202614482cb392d858175ffe9f117d38") + + inital = crc16_calc_inital(protocol_hash) + checksum = calc_crc_sum(protocol_data + b"\xaa", inital) + ref_data = protocol_hash + protocol_data + assert crc16(ref_data, 0, len(ref_data)) != checksum + + protocol_hash = bytes.fromhex("4a07789a41ef64c38a7edb78574497d4") + protocol_data = bytes.fromhex("ca321a923e202614482cb392d858175ffe9f117d38") + + inital = crc16_calc_inital(protocol_hash[-1:] + b"\xaa") + checksum = calc_crc_sum(protocol_data, inital) + ref_data = protocol_hash + protocol_data + assert crc16(ref_data, 0, len(ref_data)) != checksum diff --git a/test/output/test_encdec.py b/test/output/test_encdec.py new file mode 100644 index 0000000..2940406 --- /dev/null +++ b/test/output/test_encdec.py @@ -0,0 +1,197 @@ +import sys +sys.path.append('src/') +sys.path.append('test/') +sys.path.append('test/backend_output/python') + + +def test_HCP_encdec_MessageMotionUpdate(): + # ignore: F401 + from backend_output.python.HCP_protocol_packets import HcpMessageMotionUpdate + from backend_output.python.HCP_protocol_bitfield import HcpBitfieldEnables + from backend_output.python.HCP_protocol_enum import HcpEnumInlineMotionUpdateDangerLvl + + src_msg = HcpMessageMotionUpdate( + speed=[2, 3, 4], + stearing=0.3, + name="Hello HCP!", + enable=[True, False, True, False], + heading="N", + enables=HcpBitfieldEnables( + a=True, b=False, c=True + ), + danger_lvl=HcpEnumInlineMotionUpdateDangerLvl.ERROR + ) + + data = src_msg.to_bytes() + + isinstance(data, bytes) + + dest_msg = HcpMessageMotionUpdate().from_bytes(data) + + assert isinstance(dest_msg.speed, list) + assert isinstance(dest_msg.speed[0], int) + assert src_msg.speed == dest_msg.speed + + assert isinstance(dest_msg.stearing, float) + assert round(src_msg.stearing * 1000) == round(dest_msg.stearing * 1000) + + assert isinstance(dest_msg.name, str) + assert src_msg.name == dest_msg.name + + assert isinstance(dest_msg.enable, list) + assert isinstance(dest_msg.enable[0], bool) + assert src_msg.enable == dest_msg.enable + + assert isinstance(dest_msg.heading, str) + assert src_msg.heading == dest_msg.heading + + assert isinstance(dest_msg.enables.a, bool) + assert isinstance(dest_msg.enables.b, bool) + assert isinstance(dest_msg.enables.c, bool) + assert src_msg.enables.a == dest_msg.enables.a + assert src_msg.enables.b == dest_msg.enables.b + assert src_msg.enables.c == dest_msg.enables.c + + assert isinstance(src_msg.danger_lvl, HcpEnumInlineMotionUpdateDangerLvl) + assert src_msg.danger_lvl.value == dest_msg.danger_lvl.value + + +def test_HCP_encdec_MessageMotionUpdate_failure(): + # ignore: F401 + from backend_output.python.HCP_protocol_packets import HcpMessageMotionUpdate + from backend_output.python.HCP_protocol_bitfield import HcpBitfieldEnables + from backend_output.python.HCP_protocol_enum import HcpEnumInlineMotionUpdateDangerLvl + from backend_output.python.bproto_error import bproto_PackageErrorInvalidMessageID, bproto_PackageErrorInvalidSize + + src_msg = HcpMessageMotionUpdate( + speed=[2, 3, 4], + stearing=0.3, + name="Hello HCP!", + enable=[True, False, True, False], + heading="N", + enables=HcpBitfieldEnables( + a=True, b=False, c=True + ), + danger_lvl=HcpEnumInlineMotionUpdateDangerLvl.ERROR + ) + + data = src_msg.to_bytes() + + try: + HcpMessageMotionUpdate().from_bytes(b"\xaa" + data[1:]) + except Exception as e: + isinstance(e, bproto_PackageErrorInvalidMessageID) + + data = src_msg.to_bytes() + + try: + HcpMessageMotionUpdate().from_bytes(data[:10]) + except Exception as e: + isinstance(e, bproto_PackageErrorInvalidSize) + + +def test_HCP_encdec_HcpMessageError(): + # ignore: F401 + from backend_output.python.HCP_protocol_packets import HcpMessageError + from backend_output.python.HCP_protocol_bitfield import HcpBitfieldInlineErrorEnables + from backend_output.python.HCP_protocol_enum import HcpEnumInlineErrorRecoveryStatus + + src_msg = HcpMessageError( + recovery_status=HcpEnumInlineErrorRecoveryStatus.YES, + enables=HcpBitfieldInlineErrorEnables( + test=True, asd=False, b=True, c=False, aa=True + ) + ) + + data = src_msg.to_bytes() + + isinstance(data, bytes) + + dest_msg = HcpMessageError().from_bytes(data) + + assert src_msg.recovery_status.value == dest_msg.recovery_status.value + + assert src_msg.enables.test == dest_msg.enables.test + assert src_msg.enables.asd == dest_msg.enables.asd + assert src_msg.enables.b == dest_msg.enables.b + assert src_msg.enables.c == dest_msg.enables.c + assert src_msg.enables.aa == dest_msg.enables.aa + + +def test_HCP_encdec_HcpMessageError_failure(): + # ignore: F401 + from backend_output.python.HCP_protocol_packets import HcpMessageError + from backend_output.python.HCP_protocol_bitfield import HcpBitfieldInlineErrorEnables + from backend_output.python.HCP_protocol_enum import HcpEnumInlineErrorRecoveryStatus + from backend_output.python.bproto_error import bproto_PackageErrorInvalidMessageID, bproto_PackageErrorInvalidSize + + src_msg = HcpMessageError( + recovery_status=HcpEnumInlineErrorRecoveryStatus.YES, + enables=HcpBitfieldInlineErrorEnables( + test=True, asd=False, b=True, c=False, aa=True + ) + ) + + data = src_msg.to_bytes() + + try: + HcpMessageError().from_bytes(b"\xaa" + data[1:]) + except Exception as e: + isinstance(e, bproto_PackageErrorInvalidMessageID) + + data = src_msg.to_bytes() + + try: + HcpMessageError().from_bytes(data[:3]) + except Exception as e: + isinstance(e, bproto_PackageErrorInvalidSize) + + +def test_generic_parse_package(): + # ignore: F401 + from backend_output.python.HCP_protocol_packets import HcpMessageError, HcpMessageMotionUpdate, parse_package + from backend_output.python.HCP_protocol_bitfield import HcpBitfieldInlineErrorEnables, HcpBitfieldEnables + from backend_output.python.HCP_protocol_enum import HcpEnumInlineErrorRecoveryStatus, HcpEnumInlineMotionUpdateDangerLvl + from backend_output.python.bproto_error import bproto_PackageErrorInvalidMessageID, bproto_PackageErrorInvalidSize + + src_msg1 = HcpMessageError( + recovery_status=HcpEnumInlineErrorRecoveryStatus.YES, + enables=HcpBitfieldInlineErrorEnables( + test=True, asd=False, b=True, c=False, aa=True + ) + ) + + src_msg2 = HcpMessageMotionUpdate( + speed=[2, 3, 4], + stearing=0.3, + name="Hello HCP!", + enable=[True, False, True, False], + heading="N", + enables=HcpBitfieldEnables( + a=True, b=False, c=True + ), + danger_lvl=HcpEnumInlineMotionUpdateDangerLvl.ERROR + ) + + dest_msg1, parsed_size = parse_package(src_msg1.to_bytes()) + assert parsed_size == 5 + assert isinstance(dest_msg1, HcpMessageError) + + dest_msg2, parsed_size = parse_package(src_msg2.to_bytes()) + assert parsed_size == 58 + assert isinstance(dest_msg2, HcpMessageMotionUpdate) + + try: + parse_package(b"\xaa" + src_msg2.to_bytes()[1:]) + except Exception as e: + isinstance(e, bproto_PackageErrorInvalidMessageID) + + try: + parse_package(src_msg1.to_bytes()[:2]) + except Exception as e: + isinstance(e, bproto_PackageErrorInvalidSize) + + try: + parse_package(src_msg2.to_bytes()[:2]) + except Exception as e: + isinstance(e, bproto_PackageErrorInvalidSize) diff --git a/test/reference/python/HCP_protocol_bitfield.py b/test/reference/python/HCP_protocol_bitfield.py new file mode 100644 index 0000000..ee45150 --- /dev/null +++ b/test/reference/python/HCP_protocol_bitfield.py @@ -0,0 +1,82 @@ +from bproto_base import bproto_Bitfield + + +class HcpBitfieldEnables(bproto_Bitfield): + BYTE_SIZE = 1 + + def __init__(self, + a=False, + b=False, + c=False, + ): + self.a = a + self.b = b + self.c = c + + def to_bytes(self) -> bytes: + res = 0 + res |= (1 << 0) if self.a else 0 + res |= (1 << 1) if self.b else 0 + res |= (1 << 3) if self.c else 0 + return int.to_bytes(res, self.BYTE_SIZE, 'little') + + def from_bytes(self, data: bytes): + bits = int.from_bytes(data[:self.BYTE_SIZE], 'little') + self.a = (bits & (1 << 0)) != 0 + self.b = (bits & (1 << 1)) != 0 + self.c = (bits & (1 << 3)) != 0 + return self + + def __repr__(self) -> str: + value_str = ", ".join([ + "a=" + str(self.a), + "b=" + str(self.b), + "c=" + str(self.c), + ]) + return "HcpBitfieldEnables(" + value_str + ")" + + +class HcpBitfieldInlineErrorEnables(bproto_Bitfield): + BYTE_SIZE = 1 + + def __init__(self, + test=False, + asd=False, + b=False, + c=False, + aa=False, + ): + self.test = test + self.asd = asd + self.b = b + self.c = c + self.aa = aa + + def to_bytes(self) -> bytes: + res = 0 + res |= (1 << 0) if self.test else 0 + res |= (1 << 1) if self.asd else 0 + res |= (1 << 2) if self.b else 0 + res |= (1 << 3) if self.c else 0 + res |= (1 << 7) if self.aa else 0 + return int.to_bytes(res, self.BYTE_SIZE, 'little') + + def from_bytes(self, data: bytes): + bits = int.from_bytes(data[:self.BYTE_SIZE], 'little') + self.test = (bits & (1 << 0)) != 0 + self.asd = (bits & (1 << 1)) != 0 + self.b = (bits & (1 << 2)) != 0 + self.c = (bits & (1 << 3)) != 0 + self.aa = (bits & (1 << 7)) != 0 + return self + + def __repr__(self) -> str: + value_str = ", ".join([ + "test=" + str(self.test), + "asd=" + str(self.asd), + "b=" + str(self.b), + "c=" + str(self.c), + "aa=" + str(self.aa), + ]) + return "HcpBitfieldInlineErrorEnables(" + value_str + ")" + diff --git a/test/reference/python/HCP_protocol_enum.py b/test/reference/python/HCP_protocol_enum.py new file mode 100644 index 0000000..511e095 --- /dev/null +++ b/test/reference/python/HCP_protocol_enum.py @@ -0,0 +1,19 @@ +import enum + + +class HcpEnumErrorCodes(enum.Enum): + MOTOR1_DISCONNECTED = 1 + MOTOR2_DISCONNECTED = 2 + MOTORS_DISCONNECTED = 3 + INPUT_BUFFER_OVERFLOW = 4 + + +class HcpEnumInlineMotionUpdateDangerLvl(enum.Enum): + WARNING = 0 + ERROR = 1 + MELTDOWN = 2 + + +class HcpEnumInlineErrorRecoveryStatus(enum.Enum): + YES = 0 + NO = 1 diff --git a/test/reference/python/HCP_protocol_message_ids.py b/test/reference/python/HCP_protocol_message_ids.py new file mode 100644 index 0000000..9b0fb0b --- /dev/null +++ b/test/reference/python/HCP_protocol_message_ids.py @@ -0,0 +1,12 @@ +import enum + + +class MessageIds_HCP(enum.Enum): + MSG_ID_HCP_MESSAGE_MOTION_UPDATE = 1 + MSG_ID_HCP_MESSAGE_ERROR = 2 + + +MESSAGE_SIZE_MAP_HCP = { + MessageIds_HCP.MSG_ID_HCP_MESSAGE_MOTION_UPDATE: 58, + MessageIds_HCP.MSG_ID_HCP_MESSAGE_ERROR: 5, +} diff --git a/test/reference/python/HCP_protocol_packets.py b/test/reference/python/HCP_protocol_packets.py new file mode 100644 index 0000000..15758bd --- /dev/null +++ b/test/reference/python/HCP_protocol_packets.py @@ -0,0 +1,134 @@ +from bproto_base import bproto_Package, calc_crc_sum +from bproto_error import bproto_Error, bproto_PackageErrorInvalidSize, bproto_PackageErrorInvalidMessageID, bproto_PackageErrorInvalidCRC + +from HCP_protocol_enum import HcpEnumErrorCodes, HcpEnumInlineMotionUpdateDangerLvl, HcpEnumInlineErrorRecoveryStatus +from HCP_protocol_bitfield import HcpBitfieldEnables, HcpBitfieldInlineErrorEnables +from HCP_protocol_message_ids import MessageIds_HCP, MESSAGE_SIZE_MAP_HCP + +from typing import Annotated, List +import struct + + +class HcpMessageMotionUpdate(bproto_Package): + ID = MessageIds_HCP.MSG_ID_HCP_MESSAGE_MOTION_UPDATE + SIZE = 58 + + speed: Annotated[List[int], 3] + stearing: float + name: str + enable: Annotated[List[bool], 4] + heading: str + enables: HcpBitfieldEnables + danger_lvl: HcpEnumInlineMotionUpdateDangerLvl + + def __init__(self, + speed: Annotated[List[int], 3] = 0, + stearing: float = 0.0, + name: str = '', + enable: Annotated[List[bool], 4] = False, + heading: str = '', + enables: HcpBitfieldEnables = HcpBitfieldEnables(), + danger_lvl: HcpEnumInlineMotionUpdateDangerLvl = HcpEnumInlineMotionUpdateDangerLvl.WARNING, + ): + self.speed = speed + self.stearing = stearing + self.name = name + self.enable = enable + self.heading = heading + self.enables = enables + self.danger_lvl = danger_lvl + + def to_bytes(self) -> bytes: + res = struct.pack(">B", self.ID.value) + res += struct.pack('>iii', self.speed[0], self.speed[1], self.speed[2]) + res += struct.pack('>f', self.stearing) + res += self.name.encode('ascii')[:32].ljust(32, b'\x00') + res += struct.pack('>BBBB', self.enable[0], self.enable[1], self.enable[2], self.enable[3]) + res += self.heading.encode('ascii')[:1].ljust(1, b'\x00') + res += self.enables.to_bytes() + res += struct.pack('>B', self.danger_lvl.value) + res += calc_crc_sum(res) + return res + + def from_bytes(self, data: bytes): + # msg_id = struct.unpack('>B', data[:1]) + data = data[1:] + self.speed = list(struct.unpack('>iii', data[:12])) + data = data[12:] + self.stearing = struct.unpack('>f', data[:4])[0] + data = data[4:] + self.name = data[:32].decode('ascii').strip('\x00') + data = data[32:] + self.enable = [bool(i) for i in struct.unpack('>BBBB', data[:4])] + data = data[4:] + self.heading = data[:1].decode('ascii').strip('\x00') + data = data[1:] + self.enables = HcpBitfieldEnables().from_bytes(data[:1]) + data = data[1:] + self.danger_lvl = HcpEnumInlineMotionUpdateDangerLvl(struct.unpack('>B', data[:1])[0]) + data = data[1:] + # crc = data[:2] + return self + + +class HcpMessageError(bproto_Package): + ID = MessageIds_HCP.MSG_ID_HCP_MESSAGE_ERROR + SIZE = 5 + + recovery_status: HcpEnumInlineErrorRecoveryStatus + enables: HcpBitfieldInlineErrorEnables + + def __init__(self, + recovery_status: HcpEnumInlineErrorRecoveryStatus = HcpEnumInlineErrorRecoveryStatus.YES, + enables: HcpBitfieldInlineErrorEnables = HcpBitfieldInlineErrorEnables(), + ): + self.recovery_status = recovery_status + self.enables = enables + + def to_bytes(self) -> bytes: + res = struct.pack(">B", self.ID.value) + res += struct.pack('>B', self.recovery_status.value) + res += self.enables.to_bytes() + res += calc_crc_sum(res) + return res + + def from_bytes(self, data: bytes): + # msg_id = struct.unpack('>B', data[:1]) + data = data[1:] + self.recovery_status = HcpEnumInlineErrorRecoveryStatus(struct.unpack('>B', data[:1])[0]) + data = data[1:] + self.enables = HcpBitfieldInlineErrorEnables().from_bytes(data[:1]) + data = data[1:] + # crc = data[:2] + return self + + +def parse_package(data: bytes) -> tuple[bproto_Package, int]: + if len(data) < 1: + raise bproto_PackageErrorInvalidSize("Package has no data") + + msg_id = struct.unpack('>B', data[:1]) + + if msg_id not in MessageIds_HCP: + raise bproto_PackageErrorInvalidMessageID(f"Message ID '{msg_id}' is invaild") + + msg_id = MessageIds_HCP(msg_id) + expected_size = MESSAGE_SIZE_MAP_HCP.get(msg_id) or 0 + + if expected_size > len(data): + raise bproto_PackageErrorInvalidSize(f"Package is to short for '{msg_id}' ({expected_size} > {len(data)})") + + pkg_data = data[:expected_size] + + crc_data = pkg_data[-2:] + + if calc_crc_sum(pkg_data[:-2]) != crc_data: + raise bproto_PackageErrorInvalidCRC(f"CRC {crc_data.hex()} did not match calculated") + + match(msg_id): + case MessageIds_HCP.MSG_ID_HCP_MESSAGE_MOTION_UPDATE: + return HcpMessageMotionUpdate().from_bytes(pkg_data), expected_size + case MessageIds_HCP.MSG_ID_HCP_MESSAGE_ERROR: + return HcpMessageError().from_bytes(pkg_data), expected_size + case _: + raise bproto_Error("Message ID could not be interpreted; this should not have happen; its a developer error! Create a issue") diff --git a/test/reference/txt/protocolSummary.txt b/test/reference/txt/protocolSummary.txt new file mode 100644 index 0000000..3978b9d --- /dev/null +++ b/test/reference/txt/protocolSummary.txt @@ -0,0 +1,49 @@ +Name: Hcp +Version: 0 + +Enums: + ErrorCodes: + - MOTOR1_DISCONNECTED = 1 + - MOTOR2_DISCONNECTED = 2 + - MOTORS_DISCONNECTED = 3 + - INPUT_BUFFER_OVERFLOW = 4 + + InlineMotionUpdateDangerLvl: + - WARNING = 0 + - ERROR = 1 + - MELTDOWN = 2 + + InlineErrorRecoveryStatus: + - YES = 0 + - NO = 1 + +Bitfield: + Enables: + - a: 0 + - b: 1 + - c: 3 + + InlineErrorEnables: + - test: 0 + - asd: 1 + - b: 2 + - c: 3 + - aa: 7 + +Messages: + [1] MotionUpdate: + Size: 55 bytes + Fields: + - (12) speed: int32[3] + - (4) stearing: float32[1] + - (32) name: string[32] + - (4) enable: bool[4] + - (1) heading: char[1] + - (1) enables: bitfield[1] -> Enables + - (1) dangerLvl: enum[1] -> InlineMotionUpdateDangerLvl + + [2] Error: + Size: 2 bytes + Fields: + - (1) recoveryStatus: enum[1] -> InlineErrorRecoveryStatus + - (1) enables: bitfield[1] -> InlineErrorEnables diff --git a/test/runner.py b/test/runner.py new file mode 100644 index 0000000..96aba1e --- /dev/null +++ b/test/runner.py @@ -0,0 +1,4 @@ +import pytest + +if __name__ == '__main__': + pytest.main() diff --git a/test/test_protocol.bproto b/test/test_protocol.bproto new file mode 100644 index 0000000..50d0bcc --- /dev/null +++ b/test/test_protocol.bproto @@ -0,0 +1,39 @@ +protocol HCP version 0 + +enum error_codes { + MOTOR1_disconnected = 1, + MOTOR2_disconnected = 2, + MOTORS_disconnected = 3, + input_buffer_overflow = 4, +} + +bits enables { + a, b, c: 3 +} + +message [1] MotionUpdate { + [0] speed : int32[3], + [1] stearing : float32, + [3] enable : bool[4], + [4] heading : char, + [2] name : string[32], + [5] enables : bits enables, + [6] dangerLvl: enum { + WARNING, + ERROR, MELTDOWN + } +} + + +message [2] Error { + [0] recoveryStatus: enum { + YES, NO + }, + [1] enables: bits { + test : 0, + asd : 1, + b, + c, + aa: 7 + } +} diff --git a/vscode/bproto/.vscode/launch.json b/vscode/bproto/.vscode/launch.json new file mode 100644 index 0000000..44a86ab --- /dev/null +++ b/vscode/bproto/.vscode/launch.json @@ -0,0 +1,17 @@ +// A launch configuration that launches the extension inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ] + } + ] +} diff --git a/vscode/bproto/.vscodeignore b/vscode/bproto/.vscodeignore new file mode 100644 index 0000000..f369b5e --- /dev/null +++ b/vscode/bproto/.vscodeignore @@ -0,0 +1,4 @@ +.vscode/** +.vscode-test/** +.gitignore +vsc-extension-quickstart.md diff --git a/vscode/bproto/CHANGELOG.md b/vscode/bproto/CHANGELOG.md new file mode 100644 index 0000000..ac8ffc5 --- /dev/null +++ b/vscode/bproto/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +All notable changes to the "bproto" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] + +- Initial release \ No newline at end of file diff --git a/vscode/bproto/LICENSE b/vscode/bproto/LICENSE new file mode 100644 index 0000000..9e71eef --- /dev/null +++ b/vscode/bproto/LICENSE @@ -0,0 +1,7 @@ +Copyright 2025 AlexanderHD27 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vscode/bproto/README.md b/vscode/bproto/README.md new file mode 100644 index 0000000..dc5e228 --- /dev/null +++ b/vscode/bproto/README.md @@ -0,0 +1,3 @@ +# bproto Syntax Highligh + +Just a simple plugin provding syntax highligh for the bproto protocol description langunage \ No newline at end of file diff --git a/vscode/bproto/bproto-0.0.1.vsix b/vscode/bproto/bproto-0.0.1.vsix new file mode 100644 index 0000000000000000000000000000000000000000..e858b1712bc05a36d31d68607bce0ea1bf212019 GIT binary patch literal 4779 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 U_}EGU0L0iU8rvk7DNcX=2aG^31ONa4 literal 0 HcmV?d00001 diff --git a/vscode/bproto/bproto-0.0.2.vsix b/vscode/bproto/bproto-0.0.2.vsix new file mode 100644 index 0000000000000000000000000000000000000000..4bf3f2d5b7570213bbbf4538fabc45c5df6edb7f GIT binary patch literal 4778 zcmai22T)UMw++23AP6Ez5s(f7f^?9gROwZa0HOELBT6XJloG0;i4^HIfHdhKNbkM3 zTtx^dL1}-u_n~m-fA8%xXU><&%$j}nx7XV1JDRFExKscDAtB&irG+tQtU}@qHUO|n z003ME001B#50Eq1&c&I}3vB1(1a!8u27x__^=6z`n8^K|o3K%{$=Q$fLQM6Lpe*%* zN63A*LLxQ#YFeJ=+OHF(j$KhH&fXA++`j8N{YxQ0)1(!=XwVm=u127hK`Gp-0vb-f z4jhumqUlCaWCZ#IQr$5h#r9{qs@`Ka${#24MnpRIGx;vKv%$~;U%DsWb$muKEVwVZ zo!UMn%aXeZIY#R>%}DBn+}Opzl?pe@$z4~DijF$K#d$=ic|wnhRk)@*3A>w&2%xfz z$VFQ!bwBF#NSC`r=;HVg_0ZY0K8 zGxM9UtZCVnsY-LA!nZmGb+qtNk+Nd7H;7yrj;8_XArH82k`=daPG|Qpv0Z&j@(MX1 z=b`_D5hALk^PPI#mX$7Zu%;n{nJ2m@iQL~nsahdK+{gs2`=h&?ZiA8OmI;>evC=2(+%cWC9a7dZ2u;7 z99NhbF3uLOY|?Wx^pTPK(X**k24O))ZfK|F@GVY}TV$o%))QKjxczYCoZMm&!~hkG z!a}?}ZrM`$){;YeVnb5gv;H+E(tD<%^NC+Hu;$xeDYGu%qZ(x2@#Ai)R>9mu@x5Qr zu;;!G9JTSonP^a`BzlEcg@5)#E1PuAtPWzOPR8)PV1`glE zS8^=gzc5?S?>80_lkh2slhM4?%t{R^M7@|M&cp%$u3?@g@-EJpcJ_Fx>+1>voAUWM zIr_)QX|xNFDx$#=9ETzycn|Wncmi&^V#U>^34}9rq%+UnQnv{;9zQv4Ode z2`NckR|&*DT{#vpbh9n$_-4Orxcz(~%Ut7Dl7P{k-{X0!ppaC{owv~LVw)P%MwL7w zj$V#m1eu0oV^rvZjM}nY8MFO}>9Fg;2mNl-PC6;sIm;)!>=5VlrRv~xX z0?JF=ujA9qG1OmKwvWR9LWCw|OEMXb)VY>9rzjdKkr9pj%jw7RZH*DuALXxTQZ`n> zW0d`;t%S4|HEmkN@ZiXO8>d<+c*^oq)N~gY(w+?Uec!JVXAV{#CV(pCpN$XWJ8em` z_z*$y`k~JU@e>?PRRY4*bz6UJO!K|ODC(cx5ya^1@2$ZsF6k`4E6~ybXanN22fH{s z!TOxrn8<^-pGQz{z&}XoiJ=YkV*Ore+gZ90CiJm1$7XSCn_->gkC;76KDL(RnI#4Q z%WjwBxgnx(BH!oTo1VE|%2-IzkfkNX6*w?&>;<&yp3sokJFjQ0#qc@?j&w;MohV9xO(t!~WH;A=g~}cJSp%z* zDfw-$hR8p)D_UuvVA+e@DfSB;nP0Om&?>RsovEbg_F~*(Nh6=D+qOC_tty=w{!WKY zCMQjKy-jRl$KaYPQ&V>s=^Uat?g_)-GLrwBkru^#j$eEtpenV=+tG0L=O9X$6DDj-kw(Fm+h@6h5cl% z2eXat*uwFqCfi4yC#Oaq9RUG!H}LoA>_R`Q?Q*HM+vRmID>}Qy2iW>=(DwUvZuQ@k z_K6Xahumy2Yr}=KvvM}ySYR8uYrI6rACcUt6)4Tt{_r-a;VH+KEJ(SdP~*wZJR;&1 zlQI{eri7iw3n55^VSeAy%oc#FMmM0EEfXGXN;8p>&i?L7t{95<^|$)e-A^)uZ0}9; zx%9ur7OEv?y#9xnls{(I*OIsa)h>9GUxLHcto0;VYCO?P~ zK9#rXdToq~Z1&%UK>L1>oK-aPu+8igQ?G*G6+L$cZUq*kPcZ*@V(v3R{i7-FAfS~K zh|kIDUauT+n*b@^kiQ0sIf-*_AGc)d6Sf{@H9c&GX4*=qu&c-iQX|ly@!DgtWi&XS z_cJ(3wskRDHRLm^zE4eK8D(ciO{8UIl^(jv}9jkB6z&obwD5)yDyyXGtkmo;o>p!{74MErbbJ)ZlBd$!u$PCP6FH z?O#k^KL1c=e^ggpU(qYJu+4dl0#b+Z+d9 z=*=9?H}Fku!rR93F!N%8iE!SozgtL2RDyU%ubyx__$5cXg|Th zlF%zH-ue$mve0u#^CzQsky~4~*wXDSi$771AuWwgL z6rJWqlw*3@{g}|2{#wwzrBw8o>AW&mm#%%oXuRh(!3&>^cKol8<0^pyH%1`18PqS} ze7Q-8kLZ=nZ5>2^{d}&%(u`v+K2GCalUl#~$CAAvuZ^BPim1}0##gOxyC@{UoW0-k z&5qgo<5%TJ`HZEOH{TbbU-TAEsf|$;Cl`{%QWbTmM40FgaWTLU=39F!I#4p5Dno9^ zu=wCY-gf$67gQRm0B~FmLpU+BPP&f?7*m@!IF3`VkjxfQf6%m@gmD_n5wQ-DP-i)@ zm}nb~2Xni((dAa-t*fEh#b_vTxml>WC&*$Ai`P^TRKW&|fkM(rulP4%?w)v^EqI%} zYs60KaCX($s)CT-6P5C5mFhfd`ypN3>JOQ=w6%7dMiDMrBEh^Qx>m~9g|mVS@_QX< zyi?%ocRJ(WA}SIN;HE?t^u09+GVF4oUCMYf&RYss(i0 zB`j7aPiwx9sVXYB9lTRttodwE*>pytc(QdYPYi+BaL=XxoJ2tt8S=T9enn#L%hGRY zi_s;Jql*oOM|%C`&aJZKibKzvmWf0&0ca#k0iiLMLb;ht19zv;_&3AVz1!rR!|Chu zS3>DMSu{jKcG$4*MF){1*T|fA)Er;tz-RD}-0Zn?eW)l%+imUA^7|ceip^iNjmaX(EHf)VhZ{<|Cwg9F zmb9%ECA3c7H%2w!XK z^gRMeWx}A7S{~JBEco)$di}Q`KldnL(Q*U?fN*?QBMXw+;q!{HZ)&+=T5$Jf5SE4*_#|r8uKYN{mHVM;pV2l+TdQMqbrywsCj_9DqN4pWpXUjZAMW#9{cA^uPvT9P|JYWp{NWCI zBUiqvk481z)Kup5RzQJ-7g{?ddZvJ%>`6L9*R71rLpRTSnH|5`#+#wnwu%lZ%sFIb z)?2o!2aNr?JiBIk>b^~O8{FLs84T=V1`UZeV}CSvQ&oTt8)Y&}j)kp@;dTeSUv;=a z6GM>KdN<=a=Ey)m*n2%O-4_P{@TNRdDlBXnod5lE0EX0m2LgKb_&xqFeH{2V#=lQt z{~!PW2xuQhAOFjl>>S|yBy-V4pPgjR0|D*9r2RXI|9Y;uh;`<>FS^Au@jZ`hs4d<_ ztcx!EBF34oyy%b4eC2r{puPAPG5!yic@B9#6aHRX U@i3JH0EjSGD5gos$j^TL2b4TQr~m)} literal 0 HcmV?d00001 diff --git a/vscode/bproto/language-configuration.json b/vscode/bproto/language-configuration.json new file mode 100644 index 0000000..eaf6cc4 --- /dev/null +++ b/vscode/bproto/language-configuration.json @@ -0,0 +1,23 @@ +{ + "comments": { + // symbol used for single line comment. Remove this entry if your language does not support line comments + "lineComment": "//", + // symbols used for start and end a block comment. Remove this entry if your language does not support block comments + "blockComment": [ "/*", "*/" ] + }, + // symbols used as brackets + "brackets": [ + ["{", "}"], + ["[", "]"], + ], + // symbols that are auto closed when typing + "autoClosingPairs": [ + ["{", "}"], + ["[", "]"], + ], + // symbols that can be used to surround a selection + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ] +} \ No newline at end of file diff --git a/vscode/bproto/package.json b/vscode/bproto/package.json new file mode 100644 index 0000000..3e4ed9b --- /dev/null +++ b/vscode/bproto/package.json @@ -0,0 +1,26 @@ +{ + "name": "bproto", + "displayName": "bproto Language Support", + "description": "Langauge Support for bproto Protocol Description Language", + "version": "0.0.2", + "publisher": "bproto-vscode-publisher", + "engines": { + "vscode": "^1.80.0" + }, + "categories": [ + "Programming Languages" + ], + "contributes": { + "languages": [{ + "id": "bproto", + "aliases": ["bproto", "bproto"], + "extensions": [".bproto"], + "configuration": "./language-configuration.json" + }], + "grammars": [{ + "language": "bproto", + "scopeName": "text.bproto", + "path": "./syntaxes/bproto.tmLanguage.json" + }] + } +} diff --git a/vscode/bproto/syntaxes/bproto.tmLanguage.json b/vscode/bproto/syntaxes/bproto.tmLanguage.json new file mode 100644 index 0000000..a232801 --- /dev/null +++ b/vscode/bproto/syntaxes/bproto.tmLanguage.json @@ -0,0 +1,166 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "bproto", + "patterns": [ + { "include": "#tag_line" }, + { "include": "#message_def" }, + { "include": "#enum_def" }, + { "include": "#bit_field_def" }, + + { "include": "#id_index" }, + { "include": "#strings" }, + { "include": "#keywords" }, + + { "include": "#comment" }, + { "include": "#numbers" }, + { "include": "#multiline_comment" } + ], + "repository": { + "keywords": { + "name": "keyword.control.bproto", + "match": "message|bits|enum|protocol|version" + }, + "id_index": { + "name": "constant.numeric.bproto.id", + "match": "\\[\\d+]" + }, + + "tag_line": { + "name": "bproto.tag_line", + "match": "(protocol)\\s+([a-zA-Z0-9_]+)\\s+(version)\\s+(\\d+)", + "captures": { + "1" : { "name": "keyword.control.bproto" }, + "2" : { "name": "variable.name.bproto.tag" }, + "3" : { "name": "keyword.control.bproto" }, + "4" : { "name": "constant.numeric.bproto.version" } + } + }, + + "field": { + "name": "bproto.field", + "match": "(\\[\\d+])\\s*([a-zA-z0-9_]+)\\s*(:)\\s*([a-zA-Z_0-9]+)(\\[\\d+])?\\s*,?", + "captures": { + "1" : { "name": "constant.numeric.bproto.id" }, + "2" : { "name": "variable.name.bproto.field_name" }, + "3" : { "name": "punctuation.separator.bproto.colon" }, + "4" : { "name": "support.type" }, + "5" : { "name": "constant.numeric.bproto.array_size" } + } + }, + "field_ref": { + "name": "bproto.field.ref", + "match": "(\\[\\d+])\\s*([a-zA-z0-9_]+)\\s*(:)\\s*(enum|bits)\\s*([a-zA-Z_0-9]+)\\s*,?", + "captures": { + "1" : { "name": "constant.numeric.bproto.id" }, + "2" : { "name": "variable.name.bproto.field_name" }, + "3" : { "name": "punctuation.separator.bproto.colon" }, + "4" : { "name": "keyword.control.bproto" }, + "5" : { "name": "entity.name.type.bproto" } + } + }, + "field_inline_enum": { + "name": "bproto.field.inline_enum", + "begin": "(\\[\\d+])\\s*([a-zA-z0-9_]+)\\s*(:)\\s*(enum)\\s*\\{\\s*$", + "end": "^\\s*\\}", + "beginCaptures": { + "1" : { "name": "constant.numeric.bproto.id" }, + "2" : { "name": "variable.name.bproto.field_name" }, + "3" : { "name": "punctuation.separator.bproto.colon" }, + "4" : { "name": "keyword.control.bproto" } + }, + "endCaptures": {}, + "patterns": [ + { "include": "#enum_value" } + ] + }, + "field_inline_bitfield": { + "name": "bproto.field.inline_bitfield", + "begin": "(\\[\\d+])\\s*([a-zA-z0-9_]+)\\s*(:)\\s*(bits)\\s*\\{\\s*$", + "end": "^\\s*\\}", + "beginCaptures": { + "1" : { "name": "constant.numeric.bproto.id" }, + "2" : { "name": "variable.name.bproto.field_name" }, + "3" : { "name": "punctuation.separator.bproto.colon" }, + "4" : { "name": "keyword.control.bproto" } + }, + "endCaptures": {}, + "patterns": [ + { "include": "#bitfield_value" } + ] + }, + + "message_def": { + "begin": "(message)\\s*(\\[\\d+])\\s*([a-zA-z0-9_]+)\\s+\\{\\s*$", + "end": "^\\}", + "beginCaptures": { + "1": { "name": "keyword.control.bproto" }, + "2": { "name": "constant.numeric.bproto.id" }, + "3": { "name": "entity.name.type.bproto" } + }, + "endCaptures": {}, + "patterns": [ + { "include": "#field_inline_enum" }, + { "include": "#field_inline_bitfield" }, + { "include": "#field_ref" }, + { "include": "#field" } + ] + }, + + "enum_value": { + "match": "([a-zA-z0-9_]+)\\s*(=\\s*\\d+\\s*)?,?", + "captures": { + "1": { "name": "variable.name.bproto.enum_value" }, + "2": { "name": "constant.numeric.bproto.enum_value" } + } + }, + "enum_def": { + "begin": "(enum)\\s*([a-zA-z0-9_]+)\\s*\\{\\s*$", + "end": "^\\}", + "beginCaptures": { + "1": { "name": "keyword.control.bproto" }, + "2": { "name": "entity.name.type.bproto.enum.name" } + }, + "endCaptures": {}, + "patterns": [ + { "include": "#enum_value" } + ] + }, + + "bitfield_value": { + "match": "([a-zA-z0-9_]+)\\s*(:\\s*(\\d+)\\s*)?,?", + "captures": { + "1": { "name": "variable.name.bproto.enum_value" }, + "3": { "name": "constant.numeric.bproto.bitfield_pos" } + } + }, + "bit_field_def": { + "begin": "(bits)\\s*([a-zA-z0-9_]+)\\s*\\{\\s*$", + "end": "^\\}", + "beginCaptures": { + "1": { "name": "keyword.control.bproto" }, + "2": { "name": "entity.name.type.bproto.bitfield.name" } + }, + "endCaptures": {}, + "patterns": [ + { "include": "#bitfield_value" } + ] + }, + + "comment": { + "name": "comment.line.bproto", + "match": "//.*$" + }, + "numbers": { + "name": "constant.numeric.bproto", + "match": "\\d+" + }, + "multiline_comment": { + "name": "comment.block.bproto", + "begin": "/\\*", + "end": "\\*/" + } + }, + "foldingStartMarker": "\\{\\s*$", + "foldingStopMarker": "^\\s*\\}", + "scopeName": "text.bproto" +} \ No newline at end of file diff --git a/vscode/bproto/test_protocol.bproto b/vscode/bproto/test_protocol.bproto new file mode 100644 index 0000000..a38bfb9 --- /dev/null +++ b/vscode/bproto/test_protocol.bproto @@ -0,0 +1,37 @@ +protocol HCP version 0 + +enum error_codes { + MOTOR1_disconnected = 1, + MOTOR2_disconnected = 2, + MOTORS_disconnected = 3, + input_buffer_overflow = 4, +} + +bits enables { + a, b, c +} + +message [1] MotionUpdate { + [0] speed : int32[3], + [1] stearing : float32, + [3] enable : bool[4], + [4] heading : char, + [2] name : string[32], + [5] enables : bits enables, + [6] dangerLvl: enum { + WARNING, ERROR, MELTDOWN + } +} + +message [2] Error { + [0] recoveryStatus: enum { + YES, NO + }, + [1] enables: bits { + test : 0, + asd : 1, + b, + c, + aa: 7 + } +} diff --git a/vscode/bproto/vsc-extension-quickstart.md b/vscode/bproto/vsc-extension-quickstart.md new file mode 100644 index 0000000..bc73680 --- /dev/null +++ b/vscode/bproto/vsc-extension-quickstart.md @@ -0,0 +1,29 @@ +# Welcome to your VS Code Extension + +## What's in the folder + +* This folder contains all of the files necessary for your extension. +* `package.json` - this is the manifest file in which you declare your language support and define the location of the grammar file that has been copied into your extension. +* `syntaxes/bproto.tmLanguage.json` - this is the Text mate grammar file that is used for tokenization. +* `language-configuration.json` - this is the language configuration, defining the tokens that are used for comments and brackets. + +## Get up and running straight away + +* Make sure the language configuration settings in `language-configuration.json` are accurate. +* Press `F5` to open a new window with your extension loaded. +* Create a new file with a file name suffix matching your language. +* Verify that syntax highlighting works and that the language configuration settings are working. + +## Make changes + +* You can relaunch the extension from the debug toolbar after making changes to the files listed above. +* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. + +## Add more language features + +* To add features such as IntelliSense, hovers and validators check out the VS Code extenders documentation at https://code.visualstudio.com/docs + +## Install your extension + +* To start using your extension with Visual Studio Code copy it into the `/.vscode/extensions` folder and restart Code. +* To share your extension with the world, read on https://code.visualstudio.com/docs about publishing an extension.