Init Commit: Moved bproto to seperate repo

This commit is contained in:
AlexanderHD27
2025-04-14 14:43:03 +02:00
commit 45bfc724fc
125 changed files with 10822 additions and 0 deletions

14
.coveragerc Normal file
View File

@@ -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/*

15
.flake8 Normal file
View File

@@ -0,0 +1,15 @@
[flake8]
exclude =
src/gen/**/*.py,
__pycache__,
.git
filename =
src/**/*.py,
test/**/*.py
ignore =
W293,
E501,
E402

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
.antlr
.venv
.pytest_cache
.coverage
.$*
**/__pycache__
test_out/*
**/.cmake
**/build
test/backend_output/*

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "test/output/c/src/Unity"]
path = test/output/c/src/Unity
url = https://github.com/ThrowTheSwitch/Unity

77
.vscode/launch.json vendored Normal file
View File

@@ -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"
}
]
}

33
.vscode/settings.json vendored Normal file
View File

@@ -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"
]
}

155
.vscode/tasks.json vendored Normal file
View File

@@ -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" ]
},
]
}

17
Dockerfile Normal file
View File

@@ -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" ]

7
LICENSE.md Normal file
View File

@@ -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.

122
README.md Normal file
View File

@@ -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:
<img src="docs/img/bprotoDocumentationGraphics.drawio.png" alt="Diagram showing the flow from the protocol definition via the bproto-compiler to the protocol implementation (C and Python)" width="600px">
<br>
- **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.
<br>
## 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

24
TODOs.md Normal file
View File

@@ -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

39
bprotoV1.g4 Normal file
View File

@@ -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;

3
coverage/cov_c.xml Normal file

File diff suppressed because one or more lines are too long

1554
coverage/cov_python.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/26.0.3 Chrome/130.0.6723.137 Electron/33.2.1 Safari/537.36" version="26.0.3" pages="2">
<diagram name="Core Concept" id="uytGOHupnUP-L8cOnyTT">
<mxGraphModel dx="512" dy="301" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="-I73Jp0letHz3FIaNn9U-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;targetPerimeterSpacing=6;sourcePerimeterSpacing=6;strokeWidth=2;" edge="1" parent="1" source="-I73Jp0letHz3FIaNn9U-2" target="-I73Jp0letHz3FIaNn9U-7">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="-I73Jp0letHz3FIaNn9U-2" value="&lt;b&gt;.bproto&lt;/b&gt;" style="whiteSpace=wrap;html=1;shape=mxgraph.basic.document" vertex="1" parent="1">
<mxGeometry x="120" y="125" width="60" height="70" as="geometry" />
</mxCell>
<mxCell id="-I73Jp0letHz3FIaNn9U-3" value="Protocol Description" style="text;html=1;align=center;verticalAlign=top;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="110" y="210" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="-I73Jp0letHz3FIaNn9U-7" value="" style="whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1">
<mxGeometry x="240" y="120" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="-I73Jp0letHz3FIaNn9U-6" value="" style="sketch=0;outlineConnect=0;fontColor=#232F3E;gradientColor=none;fillColor=#232F3D;strokeColor=none;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;fontSize=12;fontStyle=0;aspect=fixed;pointerEvents=1;shape=mxgraph.aws4.gear;" vertex="1" parent="1">
<mxGeometry x="250" y="129" width="59" height="59" as="geometry" />
</mxCell>
<mxCell id="-I73Jp0letHz3FIaNn9U-8" value="bproto-Compiler" style="text;html=1;align=center;verticalAlign=top;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="240" y="210" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="-I73Jp0letHz3FIaNn9U-14" value="&lt;b&gt;.py&lt;/b&gt;" style="whiteSpace=wrap;html=1;shape=mxgraph.basic.document" vertex="1" parent="1">
<mxGeometry x="380" y="170" width="40" height="50" as="geometry" />
</mxCell>
<mxCell id="-I73Jp0letHz3FIaNn9U-16" value="&lt;b&gt;.c/.h&lt;/b&gt;" style="whiteSpace=wrap;html=1;shape=mxgraph.basic.document" vertex="1" parent="1">
<mxGeometry x="380" y="100" width="40" height="50" as="geometry" />
</mxCell>
<mxCell id="-I73Jp0letHz3FIaNn9U-17" value="Protocol Implementation" style="text;html=1;align=center;verticalAlign=top;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="355" y="230" width="90" height="60" as="geometry" />
</mxCell>
<mxCell id="-I73Jp0letHz3FIaNn9U-18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;targetPerimeterSpacing=6;sourcePerimeterSpacing=6;strokeWidth=2;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="-I73Jp0letHz3FIaNn9U-7" target="-I73Jp0letHz3FIaNn9U-16">
<mxGeometry relative="1" as="geometry">
<mxPoint x="196" y="170" as="sourcePoint" />
<mxPoint x="244" y="170" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="-I73Jp0letHz3FIaNn9U-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;targetPerimeterSpacing=6;sourcePerimeterSpacing=6;strokeWidth=2;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="-I73Jp0letHz3FIaNn9U-7" target="-I73Jp0letHz3FIaNn9U-14">
<mxGeometry relative="1" as="geometry">
<mxPoint x="330" y="170" as="sourcePoint" />
<mxPoint x="390" y="125" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="zBquDBVsXlGvDgQ8uFh9" name="Compadibilty Desition Tree">
<mxGraphModel dx="717" dy="422" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
</root>
</mxGraphModel>
</diagram>
</mxfile>

75
docs/compatibility.md Normal file
View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

66
docs/serialization.md Normal file
View File

@@ -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.

221
docs/syntax.md Normal file
View File

@@ -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 <name> version <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 [<Message ID>] <Message Name> {
// List of fields
[<field ID #0>] <field name #0> : <field type #0>,
[<field ID #1>] <field name #1> : <field type #1>,
....
[<field ID #N>] <field name #N> : <field type #N>,
}
```
**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:**
`[<ID>] <Name> : <datatype>`
**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>]` | 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:**
`<datatype>[<array_size>]`
**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 <bitfield name> {
<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
[<Field ID>] <Field Name> : 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 <enum name> {
<Name #0> (= <Value #0>),
<Name #1> (= <Value #1>),
...
<Name #N> (= <Value #N>),
}
```
**Example:**
```bproto
dangerLvl: enum {
WARNING,
MELTDOWN,
ERROR = 10,
}
```
**Syntax:**
```bproto
[<Field ID>] <Field Name> : enum {
<Name #0> (= <Value #0>),
<Name #1> (= <Value #1>),
...
<Name #N> (= <Value #N>),
}
```
**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 `/* ... */`).

4
pytest.ini Normal file
View File

@@ -0,0 +1,4 @@
[pytest]
testpaths =
test/output
test/compiler

11
requirements.txt Normal file
View File

@@ -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

50
run_docker_compiler.py Executable file
View File

@@ -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)

3
scripts/docker_and_push.bash Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/bash
docker build . --tag alexanderhd27/bproto-compiler:latest
docker push alexanderhd27/bproto-compiler:latest

15
src/backend/__init__.py Normal file
View File

@@ -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()

237
src/backend/fsOutput.py Normal file
View File

@@ -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"

View File

@@ -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
),
]),
])

View File

@@ -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"
)
])

View File

@@ -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()
]
}
)]
)

54
src/compiler.py Normal file
View File

@@ -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)

113
src/errors.py Normal file
View File

@@ -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!")

62
src/gen/bprotoV1.interp Normal file
View File

@@ -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]

29
src/gen/bprotoV1.tokens Normal file
View File

@@ -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

View File

@@ -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]

106
src/gen/bprotoV1Lexer.py Normal file
View File

@@ -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 = [ "<INVALID>",
"'protocol'", "'version'", "'message'", "'{'", "','", "'}'",
"'['", "']'", "'enum'", "'bits'", "':'", "'='" ]
symbolicNames = [ "<INVALID>",
"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

View File

@@ -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

165
src/gen/bprotoV1Listener.py Normal file
View File

@@ -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

1266
src/gen/bprotoV1Parser.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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

62
src/main.py Normal file
View File

@@ -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()

169
src/nameHandling/base.py Normal file
View File

@@ -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])

View File

@@ -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)}")

View File

@@ -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

View File

@@ -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

View File

@@ -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

157
src/parser/ast_visitor.py Normal file
View File

@@ -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

120
src/parser/parser.py Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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!")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
)

View File

@@ -0,0 +1,18 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
{% 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 %}

View File

@@ -0,0 +1,18 @@
#ifndef _CRC16_H_
#define _CRC16_H_
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
uint16_t {{protocol.name}}_crc16_ccitt(const uint8_t* buffer, size_t size);
#ifdef __cplusplus
}
#endif
#endif // _CRC16_H_

View File

@@ -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 %}

View File

@@ -0,0 +1,27 @@
#pragma once
#include "{{protocol.name}}_bitfield.h"
#include "{{protocol.name}}_enum.h"
#include "{{protocol.name}}_crc.h"
#include <stdint.h>
#include <stdbool.h>
#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);

View File

@@ -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 %}

View File

@@ -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}};
}

View File

@@ -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}};
}

View File

@@ -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;
}

View File

@@ -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 <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
{% 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;
}

View File

@@ -0,0 +1,14 @@
#include "{{protocol.name}}_enum.h"
#include <stdint.h>
{{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;
}
}

View File

@@ -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 <stdint.h>
#include <stddef.h>
#include <stdbool.h>
{% 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 %}

View File

@@ -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

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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' -%}

View File

@@ -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 %}
}

View File

@@ -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 %}

View File

@@ -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' %}

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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 -%}

View File

@@ -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]

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

228
test/compiler/test_emums.py Normal file
View File

@@ -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)

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1 @@
test1

View File

@@ -0,0 +1 @@
test2

View File

@@ -0,0 +1 @@
Hello {{ name }}!

View File

@@ -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
)

17
test/output/c/src/main.c Normal file
View File

@@ -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();
}

Some files were not shown because too many files have changed in this diff Show More