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