add py
This commit is contained in:
parent
f93e31e9be
commit
1486786c21
160
py/.gitignore
vendored
Normal file
160
py/.gitignore
vendored
Normal 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
8
py/.idea/.gitignore
vendored
Normal 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
|
13
py/.idea/inspectionProfiles/Project_Default.xml
Normal file
13
py/.idea/inspectionProfiles/Project_Default.xml
Normal 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>
|
6
py/.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
py/.idea/inspectionProfiles/profiles_settings.xml
Normal 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
4
py/.idea/misc.xml
Normal 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
8
py/.idea/modules.xml
Normal 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
6
py/.idea/other.xml
Normal 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
19
py/.idea/py.iml
Normal 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
6
py/.idea/vcs.xml
Normal 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
48
py/Hello_.ini
Normal 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
8
py/README.md
Normal 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
3
py/backend.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import aiohttp
|
||||||
|
import orjson
|
||||||
|
import threading
|
47
py/gui.py
Normal file
47
py/gui.py
Normal 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
10
py/optimus.py
Normal 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
1371
py/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
35
py/pyproject.toml
Normal file
35
py/pyproject.toml
Normal 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
114
py/pytelem.py
Normal 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
459
py/skylab.py
Normal 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."""
|
40
py/templates/c/skylab.h.jinja2
Normal file
40
py/templates/c/skylab.h.jinja2
Normal 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
|
Loading…
Reference in a new issue