mapcan 0.21.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapcan/__init__.py +9 -0
- mapcan/bits.py +85 -0
- mapcan/canopen.py +64 -0
- mapcan/cli.py +598 -0
- mapcan/codegen/__init__.py +0 -0
- mapcan/codegen/c.py +451 -0
- mapcan/codegen/common.py +95 -0
- mapcan/codegen/py.py +277 -0
- mapcan/diagnostics.py +65 -0
- mapcan/diff.py +383 -0
- mapcan/docmodel.py +556 -0
- mapcan/examples/canopen-node.yaml +113 -0
- mapcan/examples/j1939.yaml +193 -0
- mapcan/examples/minimal.yaml +81 -0
- mapcan/examples/tractor/powertrain.yaml +103 -0
- mapcan/examples/tractor/tractor.yaml +52 -0
- mapcan/examples/vehicle-can.yaml +163 -0
- mapcan/export/__init__.py +0 -0
- mapcan/export/dbc.py +195 -0
- mapcan/export/eds.py +215 -0
- mapcan/export/pdf.py +144 -0
- mapcan/export/text.py +70 -0
- mapcan/export/xlsx.py +416 -0
- mapcan/extras.py +38 -0
- mapcan/fmt.py +316 -0
- mapcan/fonts/OFL-PTSerif.txt +93 -0
- mapcan/fonts/OFL.txt +93 -0
- mapcan/fonts/PT_Sans-Web-Bold.ttf +0 -0
- mapcan/fonts/PT_Sans-Web-BoldItalic.ttf +0 -0
- mapcan/fonts/PT_Sans-Web-Italic.ttf +0 -0
- mapcan/fonts/PT_Sans-Web-Regular.ttf +0 -0
- mapcan/fonts/PT_Serif-Web-Bold.ttf +0 -0
- mapcan/fonts/PT_Serif-Web-BoldItalic.ttf +0 -0
- mapcan/fonts/PT_Serif-Web-Italic.ttf +0 -0
- mapcan/fonts/PT_Serif-Web-Regular.ttf +0 -0
- mapcan/i18n.py +34 -0
- mapcan/importers/__init__.py +1 -0
- mapcan/importers/dbc.py +257 -0
- mapcan/launcher.py +332 -0
- mapcan/layout.py +46 -0
- mapcan/loader.py +583 -0
- mapcan/locale/__init__.py +1 -0
- mapcan/locale/ru.py +952 -0
- mapcan/lsp/__init__.py +1 -0
- mapcan/lsp/analysis.py +865 -0
- mapcan/lsp/edits.py +1213 -0
- mapcan/lsp/exports.py +57 -0
- mapcan/lsp/server.py +588 -0
- mapcan/model.py +802 -0
- mapcan/protocols.py +47 -0
- mapcan/resolve.py +118 -0
- mapcan/schema.py +37 -0
- mapcan/templates/document.html.j2 +207 -0
- mapcan/templates/document.md.j2 +151 -0
- mapcan/templates/pdf/gost-19.typ +188 -0
- mapcan/templates/pdf/gost-2.105.typ +242 -0
- mapcan/templates/pdf/gost-7.32.typ +196 -0
- mapcan/templates/pdf/lib.typ +332 -0
- mapcan/templates/pdf/mapcan.typ +117 -0
- mapcan/templates/xlsx_style.yaml +22 -0
- mapcan/units.py +61 -0
- mapcan/updates.py +85 -0
- mapcan/upgrade.py +159 -0
- mapcan/userconfig.py +98 -0
- mapcan/validate.py +853 -0
- mapcan/web/__init__.py +0 -0
- mapcan/web/app.py +1292 -0
- mapcan/web/examples.py +88 -0
- mapcan/web/newmap.py +160 -0
- mapcan/web/session.py +438 -0
- mapcan/web/static/OFL-Sora.txt +93 -0
- mapcan/web/static/assets/index-DRMUrO54.js +31 -0
- mapcan/web/static/assets/index-S8VlG2gr.css +1 -0
- mapcan/web/static/favicon.svg +7 -0
- mapcan/web/static/index.html +14 -0
- mapcan/web/yamlops.py +768 -0
- mapcan-0.21.0.dist-info/METADATA +492 -0
- mapcan-0.21.0.dist-info/RECORD +85 -0
- mapcan-0.21.0.dist-info/WHEEL +4 -0
- mapcan-0.21.0.dist-info/entry_points.txt +5 -0
- mapcan-0.21.0.dist-info/licenses/LICENSE +176 -0
- mapcan-0.21.0.dist-info/licenses/NOTICE +15 -0
- mapcan-0.21.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +89 -0
- mapcan-0.21.0.dist-info/licenses/src/mapcan/fonts/OFL-PTSerif.txt +93 -0
- mapcan-0.21.0.dist-info/licenses/src/mapcan/fonts/OFL.txt +93 -0
mapcan/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""mapcan: a YAML signal map as the single source of truth."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.21.0"
|
|
4
|
+
|
|
5
|
+
from .loader import load, loads # noqa: E402
|
|
6
|
+
from .model import MapCan # noqa: E402
|
|
7
|
+
from .validate import check, validate_file, validate_text # noqa: E402
|
|
8
|
+
|
|
9
|
+
__all__ = ["MapCan", "check", "load", "loads", "validate_file", "validate_text", "__version__"]
|
mapcan/bits.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Bit layout of signals and packing/unpacking of the payload.
|
|
2
|
+
|
|
3
|
+
Positions are in lsb0 numbering: absolute bit ``p`` is bit ``p % 8`` of byte ``p // 8``.
|
|
4
|
+
``start_bit`` of a signal is the position of its least significant bit (LSB).
|
|
5
|
+
|
|
6
|
+
* little-endian: the value takes bits ``start .. start+len-1`` in a row;
|
|
7
|
+
* big-endian (Motorola): from the LSB the bits go up to the end of the byte, then
|
|
8
|
+
continue at bit 0 of the *previous* byte.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def chunks(start: int, length: int, big_endian: bool) -> list[tuple[int, int, int]]:
|
|
15
|
+
"""Split a field into pieces within bytes: (byte, shift in the byte, number of bits).
|
|
16
|
+
|
|
17
|
+
The pieces go from the low bits of the value to the high ones. A byte can be
|
|
18
|
+
negative if a big-endian field sticks out before the start of the frame.
|
|
19
|
+
"""
|
|
20
|
+
out = []
|
|
21
|
+
pos, left = start, length
|
|
22
|
+
while left > 0:
|
|
23
|
+
byte, shift = divmod(pos, 8)
|
|
24
|
+
n = min(8 - shift, left)
|
|
25
|
+
out.append((byte, shift, n))
|
|
26
|
+
left -= n
|
|
27
|
+
pos = (byte - 1) * 8 if big_endian else (byte + 1) * 8
|
|
28
|
+
return out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def occupied_bits(start: int, length: int, big_endian: bool) -> list[int]:
|
|
32
|
+
"""Absolute positions of the field's bits, from the value's least significant bit to the most significant."""
|
|
33
|
+
bits = []
|
|
34
|
+
for byte, shift, n in chunks(start, length, big_endian):
|
|
35
|
+
base = byte * 8 + shift
|
|
36
|
+
bits.extend(range(base, base + n))
|
|
37
|
+
return bits
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def byte_span(start: int, length: int, big_endian: bool) -> tuple[int, int]:
|
|
41
|
+
"""First and last byte of the field (ascending)."""
|
|
42
|
+
bs = [c[0] for c in chunks(start, length, big_endian)]
|
|
43
|
+
return min(bs), max(bs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def msb_position(start: int, length: int, big_endian: bool) -> int:
|
|
47
|
+
"""Position of the value's most significant bit (needed for DBC/Motorola)."""
|
|
48
|
+
return occupied_bits(start, length, big_endian)[-1]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def lsb_from_msb(msb: int, length: int) -> int:
|
|
52
|
+
"""start_bit (LSB position) of a big-endian field from the position of its MSB, as in DBC/Motorola.
|
|
53
|
+
|
|
54
|
+
From the MSB the value goes down the byte, and from bit 0 on to bit 7 of the *next* byte."""
|
|
55
|
+
pos = msb
|
|
56
|
+
for _ in range(length - 1):
|
|
57
|
+
pos = pos + 15 if pos % 8 == 0 else pos - 1
|
|
58
|
+
return pos
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def extract(data: bytes, start: int, length: int, big_endian: bool, signed: bool = False) -> int:
|
|
62
|
+
value, i = 0, 0
|
|
63
|
+
for byte, shift, n in chunks(start, length, big_endian):
|
|
64
|
+
value |= ((data[byte] >> shift) & ((1 << n) - 1)) << i
|
|
65
|
+
i += n
|
|
66
|
+
if signed and value & (1 << (length - 1)):
|
|
67
|
+
value -= 1 << length
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def insert(buf: bytearray, value: int, start: int, length: int, big_endian: bool) -> None:
|
|
72
|
+
value &= (1 << length) - 1
|
|
73
|
+
i = 0
|
|
74
|
+
for byte, shift, n in chunks(start, length, big_endian):
|
|
75
|
+
mask = ((1 << n) - 1) << shift
|
|
76
|
+
buf[byte] = (buf[byte] & ~mask & 0xFF) | (((value >> i) << shift) & mask)
|
|
77
|
+
i += n
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def raw_range(length: int, signed: bool, code: str | None = None) -> tuple[int, int]:
|
|
81
|
+
"""Range of the raw value; in ones' complement and sign-magnitude it is symmetric (zero has two forms)."""
|
|
82
|
+
if signed:
|
|
83
|
+
lo = -(1 << (length - 1)) + (code in ("ones_complement", "sign_magnitude"))
|
|
84
|
+
return lo, (1 << (length - 1)) - 1
|
|
85
|
+
return 0, (1 << length) - 1
|
mapcan/canopen.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""CANopen function codes (CiA 301, predefined connection set)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class FunctionCode:
|
|
10
|
+
type: str
|
|
11
|
+
base: int
|
|
12
|
+
per_node: bool # ID = base + node_id
|
|
13
|
+
direction: str | None # default direction relative to the device
|
|
14
|
+
label_ru: str
|
|
15
|
+
label_en: str
|
|
16
|
+
purpose_ru: str
|
|
17
|
+
purpose_en: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
FUNCTION_CODES: dict[str, FunctionCode] = {
|
|
21
|
+
fc.type: fc
|
|
22
|
+
for fc in [
|
|
23
|
+
FunctionCode("nmt", 0x000, False, "rx", "NMT", "NMT", "управление сетью", "network management"),
|
|
24
|
+
FunctionCode("sync", 0x080, False, None, "SYNC", "SYNC", "синхронизация", "synchronization"),
|
|
25
|
+
FunctionCode("emcy", 0x080, True, "tx", "EMCY", "EMCY", "события", "emergency"),
|
|
26
|
+
FunctionCode("time", 0x100, False, None, "TIME", "TIME", "дата и время", "time stamp"),
|
|
27
|
+
FunctionCode("pdo_tx1", 0x180, True, "tx", "PDO TX 1", "TPDO1", "состояния", "states"),
|
|
28
|
+
FunctionCode("pdo_rx1", 0x200, True, "rx", "PDO RX 1", "RPDO1", "установки", "settings"),
|
|
29
|
+
FunctionCode("pdo_tx2", 0x280, True, "tx", "PDO TX 2", "TPDO2", "данные", "data"),
|
|
30
|
+
FunctionCode("pdo_rx2", 0x300, True, "rx", "PDO RX 2", "RPDO2", "", ""),
|
|
31
|
+
FunctionCode("pdo_tx3", 0x380, True, "tx", "PDO TX 3", "TPDO3", "", ""),
|
|
32
|
+
FunctionCode("pdo_rx3", 0x400, True, "rx", "PDO RX 3", "RPDO3", "", ""),
|
|
33
|
+
FunctionCode("pdo_tx4", 0x480, True, "tx", "PDO TX 4", "TPDO4", "", ""),
|
|
34
|
+
FunctionCode("pdo_rx4", 0x500, True, "rx", "PDO RX 4", "RPDO4", "", ""),
|
|
35
|
+
FunctionCode("sdo_tx", 0x580, True, "tx", "SDO TX", "SDO TX", "настройка (ответ)", "SDO response"),
|
|
36
|
+
FunctionCode("sdo_rx", 0x600, True, "rx", "SDO RX", "SDO RX", "настройка (приказ)", "SDO request"),
|
|
37
|
+
FunctionCode("heartbeat", 0x700, True, "tx", "Heartbeat", "Heartbeat", "контроль связи", "node guarding"),
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Allowed lengths of service frames by CiA 301; a frame without an explicit length gets the smallest.
|
|
42
|
+
CANOPEN_DLC: dict[str, set[int]] = {
|
|
43
|
+
"nmt": {2}, "sync": {0, 1}, "time": {6}, "heartbeat": {1}, "sdo_tx": {8}, "sdo_rx": {8},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Order of the rows in the addressing table.
|
|
47
|
+
ADDRESSING_ORDER = list(FUNCTION_CODES)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def compute_id(frame_type: str, node_id: int | None) -> int | None:
|
|
51
|
+
"""CAN ID from the function code and node_id; None if it cannot be computed."""
|
|
52
|
+
fc = FUNCTION_CODES.get(frame_type)
|
|
53
|
+
if fc is None:
|
|
54
|
+
return None
|
|
55
|
+
if not fc.per_node:
|
|
56
|
+
return fc.base
|
|
57
|
+
if node_id is None:
|
|
58
|
+
return None
|
|
59
|
+
return fc.base + node_id
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def default_direction(frame_type: str) -> str | None:
|
|
63
|
+
fc = FUNCTION_CODES.get(frame_type)
|
|
64
|
+
return fc.direction if fc else None
|