This commit is contained in:
saji 2023-06-19 21:21:47 -05:00
parent f93e31e9be
commit 1486786c21
19 changed files with 2365 additions and 0 deletions

160
py/.gitignore vendored Normal file
View file

@ -0,0 +1,160 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

8
py/.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View file

@ -0,0 +1,13 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="str.__neg__" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
py/.idea/misc.xml Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (py)" project-jdk-type="Python SDK" />
</project>

8
py/.idea/modules.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/py.iml" filepath="$PROJECT_DIR$/.idea/py.iml" />
</modules>
</component>
</project>

6
py/.idea/other.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PySciProjectComponent">
<option name="PY_SCI_VIEW_SUGGESTED" value="true" />
</component>
</project>

19
py/.idea/py.iml Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="templateFileTypes">
<list>
<option value="C++" />
<option value="HTML" />
<option value="XHTML" />
<option value="XML" />
</list>
</option>
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
</component>
</module>

6
py/.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

48
py/Hello_.ini Normal file
View file

@ -0,0 +1,48 @@
[Window][Main window (title bar invisible)]
Pos=0,0
Size=1279,911
Collapsed=0
[Window][Debug##Default]
Pos=60,60
Size=400,400
Collapsed=0
[Window][my application]
Pos=144,131
Size=503,728
Collapsed=0
DockId=0x00000002,0
[Window][Dear ImGui Demo]
Pos=649,131
Size=501,728
Collapsed=0
DockId=0x00000003,0
[Window][DockSpace Demo]
Pos=0,0
Size=1279,911
Collapsed=0
[Window][Delete?]
Pos=507,390
Size=264,130
Collapsed=0
[Window][Stacked 2]
Pos=539,417
Size=200,77
Collapsed=0
[Window][Stacked 1]
Pos=456,361
Size=367,188
Collapsed=0
[Docking][Data]
DockNode ID=0x00000001 Pos=144,131 Size=1006,728 Split=X Selected=0xE87781F4
DockNode ID=0x00000002 Parent=0x00000001 SizeRef=503,728 Selected=0xFC0731F4
DockNode ID=0x00000003 Parent=0x00000001 SizeRef=501,728 Selected=0xE87781F4
DockSpace ID=0x3BC79352 Window=0x4647B76E Pos=0,21 Size=1279,890 CentralNode=1

8
py/README.md Normal file
View file

@ -0,0 +1,8 @@
# Pytelem
this is a GUI (using DearPyGui) and a solver helper process (using numpy/scipy).
The GUI is used for clients to connect to the `gotelem` server. The solver code
can be run headless and interface with the gotelem server to use the data from the server
and provide solutions for other clients to ingest.
more to come.

3
py/backend.py Normal file
View file

@ -0,0 +1,3 @@
import aiohttp
import orjson
import threading

47
py/gui.py Normal file
View file

@ -0,0 +1,47 @@
import sys
import pyqtgraph.parametertree
from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import QDir, Qt
from PySide6.QtWidgets import (
QApplication,
QWidget,
QMainWindow,
QTreeView,
QDockWidget,
)
class MainApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hey there")
ptree = PacketTree(self)
self.setCentralWidget(ptree)
class PacketTree(QWidget):
"""PacketView is a widget that shows a tree of packets as well as properties on them when selected."""
def __init__(self, parent: QtWidgets.QWidget | None = None):
super().__init__(parent)
self.setWindowTitle("Packet Overview")
splitter = QtWidgets.QSplitter(self)
layout = QtWidgets.QVBoxLayout()
splitter.setOrientation(Qt.Vertical)
self.tree = QTreeView()
self.prop_table = pyqtgraph.parametertree.ParameterTree()
splitter.addWidget(self.tree)
splitter.addWidget(self.prop_table)
layout.addWidget(splitter)
self.setLayout(layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainApp()
main_window.show()
app.exec()

10
py/optimus.py Normal file
View file

@ -0,0 +1,10 @@
# hyperspeed analytics and planning
import numpy as np
from scipy.integrate import solve_bvp, solve_ivp
from numba import njit, vectorize, guvectorize
def fsolve_discrete(): ...

1371
py/poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

35
py/pyproject.toml Normal file
View file

@ -0,0 +1,35 @@
[tool.poetry]
name = "pytelem"
version = "0.1.0"
description = "A python helper tool for gotelem"
authors = ["saji <saji@saji.dev>"]
license = "MIT"
readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.11,<3.12"
orjson = "^3.8.14"
imgui-bundle = "^0.8.5"
numpy = "^1.24.3"
aiohttp = "^3.8.4"
pyside6 = "^6.5.1"
pydantic = "^1.10.9"
pyyaml = "^6.0"
jinja2 = "^3.1.2"
pyqtgraph = "^0.13.3"
scipy = "^1.10.1"
numba = "^0.57.0"
[tool.poetry.group.dev.dependencies]
mypy = "^1.3.0"
pytest = "^7.3.2"
types-pyyaml = "^6.0.12.10"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
pytelem = "pytelem:main"

114
py/pytelem.py Normal file
View file

@ -0,0 +1,114 @@
import time
import numpy as np
from imgui_bundle import implot, imgui_knobs, imgui, immapp, hello_imgui
import aiohttp
import orjson
# Fill x and y whose plot is a heart
vals = np.arange(0, np.pi * 2, 0.01)
x = np.power(np.sin(vals), 3) * 16
y = 13 * np.cos(vals) - 5 * np.cos(2 * vals) - 2 * np.cos(3 * vals) - np.cos(4 * vals)
# Heart pulse rate and time tracking
phase = 0
t0 = time.time() + 0.2
heart_pulse_rate = 80
class PacketState:
"""PacketState is the state representation for a packet. It contains metadata about the packet
as well as a description of the packet fields. Also contains a buffer.
"""
def render_tree(self):
"""Render the Tree view entry for the packet. Only called if the packet is shown."""
pass
def render_graphs(self):
pass
def __init__(self, name: str, description: str | None = None):
self.name = name
self.description = description
# take the data fragment and create internal data representing it.
boards = {
"bms": {
"bms_measurement": {
"description": "Voltages for main battery and aux pack",
"id": 0x10,
"data": {
"battery_voltage": 127.34,
"aux_voltage": 23.456,
"current": 1.23,
},
},
"battery_status": {
"description": "Status bits for the battery",
"id": 0x11,
"data": {
"battery_state": {
"startup": True,
"precharge": False,
"discharging": False,
"lv_only": False,
"charging": False,
"wall_charging": False,
"killed": False,
}, # repeat for rest fo fields
},
},
}
}
def gui():
global heart_pulse_rate, phase, t0, x, y
# Make sure that the animation is smooth
hello_imgui.get_runner_params().fps_idling.enable_idling = False
t = time.time()
phase += (t - t0) * heart_pulse_rate / (np.pi * 2)
k = 0.8 + 0.1 * np.cos(phase)
t0 = t
imgui.show_demo_window()
main_window_flags: imgui.WindowFlags = imgui.WindowFlags_.no_collapse.value
imgui.begin("my application", p_open=None, flags=main_window_flags)
imgui.text("Bloat free code")
if implot.begin_plot("Heart", immapp.em_to_vec2(21, 21)):
implot.plot_line("", x * k, y * k)
implot.end_plot()
for board_name, board_packets in boards.items():
if imgui.tree_node(board_name):
for packet_name in board_packets:
if imgui.tree_node(packet_name):
# display description if hovered
pkt = board_packets[packet_name]
if imgui.is_item_hovered():
imgui.set_tooltip(pkt["description"])
imgui.text(f"0x{pkt['id']:03X}")
imgui.tree_pop()
imgui.tree_pop()
imgui.end() # my application
_, heart_pulse_rate = imgui_knobs.knob("Pulse", heart_pulse_rate, 30, 180)
# class State:
# def __init__(self):
#
# def gui(self):
if __name__ == "__main__":
immapp.run(
gui,
window_size=(300, 450),
window_title="Hello!",
with_implot=True,
fps_idle=0,
) # type: ignore

459
py/skylab.py Normal file
View file

@ -0,0 +1,459 @@
# this file describes a skylab yaml and it's associated fields. It also
# provides functions to parse a skylab packet folder and a few AST operators.
from abc import ABC, abstractmethod
import re
from pathlib import Path
from typing import Callable, Iterable, NewType, TypedDict, List, Protocol, Union, Set
from pydantic import BaseModel, validator
from enum import Enum
import yaml
import jinja2
# This part of the file is dedicated to parsing the skylab yaml files. We define
# classes that represent objects in the yaml files, and perform basic validation on
# the input data. We also define a load_yamls function that loads a directory of skylab files.
class FieldType(str, Enum):
"""FieldType indicates the type of the field - the enum represents the C type,
but you can use a map to convert the type to another language."""
# used to ensure types are valid, and act as representations for other languages/mappings.
U8 = "uint8_t"
U16 = "uint16_t"
U32 = "uint32_t"
U64 = "uint64_t"
I8 = "int8_t"
I16 = "int16_t"
I32 = "int32_t"
I64 = "int64_t"
F32 = "float"
Bitfield = "bitfield"
def size(self) -> int:
"""Returns the size, in bytes, of the type."""
match self:
case FieldType.U8:
return 1
case FieldType.U16:
return 2
case FieldType.U32:
return 4
case FieldType.U64:
return 8
case FieldType.I8:
return 1
case FieldType.I16:
return 2
case FieldType.I32:
return 4
case FieldType.I64:
return 8
case FieldType.F32:
return 4
case FieldType.Bitfield:
return 1
return -1
# A FieldTypeMapper is any function that takes a field type and returns
# either a mapped string (based on the type) or none if there was not a match.
FieldTypeMapper = Callable[[FieldType], str | None]
class _Bits(TypedDict):
"""Internal class: a bits object just has a name."""
name: str
class SkylabField(BaseModel):
"""Represents a field (data element) inside a Skylab Packet."""
name: str
"the name of the field. must be alphanumeric and underscores"
type: FieldType
"the type of the field"
units: str | None
"optional descriptor of the unit representation"
conversion: float | None
"optional conversion factor to be applied when parsing"
bits: List[_Bits] | None
"if the type if a bitfield, "
@validator("bits")
def bits_must_exist_if_bitfield(cls, v, values):
if v is None and "type" in values and values["type"] is FieldType.Bitfield:
raise ValueError("bits are not present on bitfield type")
if (
v is not None
and "type" in values
and values["type"] is not FieldType.Bitfield
):
raise ValueError("bits are present on non-bitfield type")
return v
@validator("name")
def name_valid_string(cls, v: str):
if not re.match(r"^[A-Za-z0-9_]+$", v):
return ValueError("invalid name")
return v
@validator("name")
def name_nonzero_length(cls, v: str):
if len(v) == 0:
return ValueError("name cannot be empty string")
return v
class Endian(str, Enum):
"""Symbol representing the endianness of the packet"""
Big = "big"
Little = "little"
class SkylabPacket(BaseModel):
"""Represents a CAN packet. Contains SkylabFields with information on the structure of the data."""
name: str
description: str | None
id: int
endian: Endian
repeat: int | None
offset: int | None
data: List[SkylabField]
# @validator("data")
# def packet_size_limit(cls, v: List[SkylabField]):
# tot = sum([f.type.size() for f in v])
# if tot > 8:
# return ValueError("Total packet size cannot exceed 8 bytes")
# return v
@validator("id")
def id_non_negative(cls, v: int) -> int:
if v < 0:
raise ValueError("id must be above zero")
return v
@validator("name")
def name_valid_string(cls, v: str) -> str:
if not re.match(r"^[A-Za-z0-9_]+$", v):
raise ValueError("invalid name", v)
return v
@validator("name")
def name_nonzero_length(cls, v: str) -> str:
if len(v) == 0:
raise ValueError("name cannot be empty string")
return v
@validator("offset")
def offset_must_have_repeat(cls, v: int | None, values) -> int | None:
if v is not None and "repeat" in values and values["repeat"] is not None:
raise ValueError("field with offset must have repeat defined")
return v
@validator("repeat")
def repeat_gt_one(cls, v: int | None):
if v is not None and v <= 1:
raise ValueError("repeat must be strictly greater than one")
return v
class SkylabBoard(BaseModel):
"""Represents a single board. Each board has packets that it sends and receives
Validations:
- There can only be one sender of a packet, but multiple receivers
- every name in the transmit/receive list must have a corresponding packet.
"""
name: str
"The name of the board"
transmit: List[str]
"The packets sent by this board"
receive: List[str]
"The packets received by this board."
@validator("name")
def name_valid_string(cls, v: str):
if not re.match(r"^[A-Za-z0-9_]+$", v):
return ValueError("invalid name", v)
return v
@validator("name")
def name_nonzero_length(cls, v: str):
if len(v) == 0:
return ValueError("name cannot be empty string")
return v
class SkylabBus(BaseModel):
name: str
"The name of the bus"
baud_rate: int
"Baud rate setting for the bus"
extended_id: bool
"If the bus uses extended ids"
@validator("name")
def name_valid_string(cls, v: str):
if not re.match(r"^[A-Za-z0-9_]+$", v):
return ValueError("invalid name", v)
return v
@validator("baud_rate")
def baud_rate_supported(cls, v: int):
if v not in [125000, 250000, 500000, 750000, 1000000]:
raise ValueError("unsupported baud rate", v)
return v
class SkylabFile(BaseModel):
"""Represents an entire skylab yaml file. Performs additional cross-validation between
boards and packets."""
packets: List[SkylabPacket] = []
boards: List[SkylabBoard] = []
busses: List[SkylabBus] = []
# TODO: add extra validators here?
def load_skylab_dir(path: Path) -> SkylabFile:
"""Loads all the .yaml files in a directory and merges them into one large SkylabFile, which is then returned."""
files = [f for f in path.iterdir() if re.search(r".*\.ya?ml$", str(f))]
sky_files: List[SkylabFile] = []
for file in files:
with open(file, "r") as f:
obj = yaml.load(f, Loader=yaml.Loader)
sky_files.append(SkylabFile.parse_obj(obj))
# merge the files
# this is not very fast or elegant but who cares.
all_pkts = []
all_boards = []
for sky_f in sky_files:
d = sky_f.dict()
all_pkts.append(d["packets"])
all_boards.append(d["boards"])
collected_skyfile = SkylabFile.parse_obj(
{"packets": all_pkts, "boards": all_boards}
)
return collected_skyfile
# hey. The next bit of code is entirely optional for you to use! It's totally acceptable to just skip it and manually
# iterate over the SkylabFile yourself. The reason we use the walk tree/visitor pattern here is to abstract away
# traversing the tree from the functions that process it.
# While this is generally a pretty common use case (hence the abstraction), it
# can be difficult to wrap certain processes around it.
SkylabObject = Union[SkylabFile, SkylabPacket, SkylabBoard, SkylabField, SkylabBus]
"SkylabObject is any object that will be walked when making a parsing pass on the AST"
class SkylabWalker(Protocol):
"""A SkylabWalker is any class that implements walk(self, SkylabObject)."""
def walk(self, obj: SkylabObject):
...
"walk is called for each SkylabObject in the SkylabFile tree"
class SkylabVisitor(ABC):
"""SkylabVisitor is an abstract class that makes children walkable. Children must
implement the visit_* functions which contain explicit signatures for each discrete unit in a Skylabfile.
"""
@abstractmethod
def visit_file(self, file: SkylabFile):
...
@abstractmethod
def visit_board(self, board: SkylabBoard):
...
@abstractmethod
def visit_packet(self, packet: SkylabPacket):
...
@abstractmethod
def visit_field(self, field: SkylabField, parent: SkylabPacket):
...
@abstractmethod
def visit_bus(self, bus: SkylabBus):
...
_last_parent: SkylabPacket | None = None
"internal variable storing the parent packet for visit_field"
def walk(self, obj: SkylabObject):
match obj:
case SkylabFile():
self.visit_file(obj)
case SkylabBoard():
self.visit_board(obj)
case SkylabPacket():
self._last_parent = obj
self.visit_packet(obj)
case SkylabBus():
self.visit_bus(obj)
case SkylabField():
if self._last_parent is None:
raise Error("Unexpected field without parent")
self.visit_field(obj, self._last_parent)
def walk_tree(tree: SkylabFile, walker: SkylabWalker):
"""Walks the tree using the given walker"""
walker.walk(tree)
for bus in tree.busses:
walker.walk(bus)
for board in tree.boards:
walker.walk(board)
for packet in tree.packets:
walker.walk(packet)
for field in packet.data:
walker.walk(field)
class Error(Exception):
"""An exception when processing the tree"""
class RelationValidator:
"""This class is a processor that validates the relation between boards and
packets.
- Each packet MAY have AT MOST one transmitter.
- Each packet MUST have AT LEAST one board reference.
- Boards MUST ONLY reference packets that exist in 'packets'
- Boards MUST have a UNIQUE name
- Packets MUST have a UNIQUE name"""
seen_packets: Set[str]
"A set of all the packets in the 'packets' field"
sent_packets: Set[str]
"A set of all the packet names that are sent by boards"
recv_packets: Set[str]
"A set of all packets recv'd by boards"
board_names: Set[str]
def __init__(self):
self.board_names = set()
self.seen_packets = set()
self.sent_packets = set()
self.board_names = set()
# The first test: make sure that no two boards send the same packet.
# check sent_packets for existing element before adding.
# the second test: each packet should have a unique name.
# the third test -> Union of sent_packets and recv_packets should
# be exactly equal to seen_packets
def walk(self, obj: SkylabObject):
match obj:
case SkylabPacket(name=n):
if n in self.seen_packets:
raise Error(f"packet {n} declared twice")
self.seen_packets.add(n)
case SkylabBoard(transmit=tx, receive=rx, name=n):
if n in self.board_names:
raise Error(f"board {n} declared twice")
self.board_names.add(n)
for r_pkt in rx:
self.recv_packets.add(r_pkt)
for t_pkt in tx:
if t_pkt in self.sent_packets:
raise Error(f"packet {t_pkt} is sent from two sources")
self.sent_packets.add(t_pkt)
case _:
... # skip others.
def validate(self):
"""runs final checks"""
# perform third check: packets must all be used.
board_ref_packets = self.recv_packets.union(self.sent_packets)
# xor = symmetric_difference, which is packets in one or the other but not both.
unref_packets = board_ref_packets ^ self.seen_packets
if len(unref_packets) > 0:
raise Error(f"packets missing a link: {unref_packets}")
class CollisionDetector:
"""This class detects ID collisions of packets. It expands the repeated packet to ensure that there is
no overlap"""
seen_ids: Set[int] = set()
def add_or_fail(self, idx: int):
if idx in self.seen_ids:
raise ValueError(f"Collision on packet {idx}")
self.seen_ids.add(idx)
def walk(self, obj: SkylabObject):
match obj:
case SkylabPacket(id=idx, offset=None, repeat=None):
# matches single packets - just add it directly.
self.add_or_fail(idx)
# we need the guard clause cause pyright/mypy isn't smart enough to read the entire match block.
case SkylabPacket(
id=base_idx, offset=off, repeat=rpt
) if off is not None and rpt is not None:
for i in range(0, rpt):
self.add_or_fail(base_idx + i * off)
case _:
... # do nothing for packets or fields.
def validate(self):
print(f"{len(self.seen_ids)} packet IDs discovered with no collisions")
class ExampleGenerator:
"""Demonstrates how to use Jinja templates with a custom environment to generate output documents from
the skylab objects."""
env: jinja2.Environment
def __init__(self):
self.env = jinja2.Environment(
loader=jinja2.loaders.PackageLoader(".templates.c")
)
def render(self, skylab: SkylabFile):
...
class GraphvizGenerator:
"""This class converts the Skylab files into a GraphViz document
detailing the flow of data as well as information about the data."""
class CGenerator:
"""This class generates C files for our microcontrollers."""
class PyGenerator:
"""This class generates a python module that can serialize/deserialize packets."""

View file

@ -0,0 +1,40 @@
// Generated by skylab2.py
#pragma once
#include "skylab2_types.h"
namespace umnsvp::skylab2 {
typedef union {
uint8_t b[4];
uint32_t i;
float f;
} can_float_union_t;
///@brief enumeration for CAN packet IDs
enum class CANPacketId : uint32_t {
{% for p in packets %}
/// {{p.description}} {# '%#x' is a python format string.#}
CAN_PACKET_{{ p.name | upper }} = {{ '%#x' % p.id}},
{% endfor %}
};
{% for p in packets %}
///@brief {{p.description}}
struct can_packet_{{ p.name | lower }} {
{% for field in p.data %}
{% if field.__class__.__name__ == "CANMessageBitfieldDef" %}
struct {
{% for subfield in field.bits %}
uint8_t {{subfield.name}}:1;
{% endfor %}
} {{ field.name | lower }};
{% else %}{{ field.data_type }} {{field.name}};{% endif %}
{% endfor %}
};
/// the length of the {{p.name | lower}} packet
constexpr size_t CAN_LENGTH_{{ p.name | upper }} = sizeof(can_packet_{{p.name | lower}});
{% endfor %}
} // namespace umnsvp::skylab2