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.
Files changed (85) hide show
  1. mapcan/__init__.py +9 -0
  2. mapcan/bits.py +85 -0
  3. mapcan/canopen.py +64 -0
  4. mapcan/cli.py +598 -0
  5. mapcan/codegen/__init__.py +0 -0
  6. mapcan/codegen/c.py +451 -0
  7. mapcan/codegen/common.py +95 -0
  8. mapcan/codegen/py.py +277 -0
  9. mapcan/diagnostics.py +65 -0
  10. mapcan/diff.py +383 -0
  11. mapcan/docmodel.py +556 -0
  12. mapcan/examples/canopen-node.yaml +113 -0
  13. mapcan/examples/j1939.yaml +193 -0
  14. mapcan/examples/minimal.yaml +81 -0
  15. mapcan/examples/tractor/powertrain.yaml +103 -0
  16. mapcan/examples/tractor/tractor.yaml +52 -0
  17. mapcan/examples/vehicle-can.yaml +163 -0
  18. mapcan/export/__init__.py +0 -0
  19. mapcan/export/dbc.py +195 -0
  20. mapcan/export/eds.py +215 -0
  21. mapcan/export/pdf.py +144 -0
  22. mapcan/export/text.py +70 -0
  23. mapcan/export/xlsx.py +416 -0
  24. mapcan/extras.py +38 -0
  25. mapcan/fmt.py +316 -0
  26. mapcan/fonts/OFL-PTSerif.txt +93 -0
  27. mapcan/fonts/OFL.txt +93 -0
  28. mapcan/fonts/PT_Sans-Web-Bold.ttf +0 -0
  29. mapcan/fonts/PT_Sans-Web-BoldItalic.ttf +0 -0
  30. mapcan/fonts/PT_Sans-Web-Italic.ttf +0 -0
  31. mapcan/fonts/PT_Sans-Web-Regular.ttf +0 -0
  32. mapcan/fonts/PT_Serif-Web-Bold.ttf +0 -0
  33. mapcan/fonts/PT_Serif-Web-BoldItalic.ttf +0 -0
  34. mapcan/fonts/PT_Serif-Web-Italic.ttf +0 -0
  35. mapcan/fonts/PT_Serif-Web-Regular.ttf +0 -0
  36. mapcan/i18n.py +34 -0
  37. mapcan/importers/__init__.py +1 -0
  38. mapcan/importers/dbc.py +257 -0
  39. mapcan/launcher.py +332 -0
  40. mapcan/layout.py +46 -0
  41. mapcan/loader.py +583 -0
  42. mapcan/locale/__init__.py +1 -0
  43. mapcan/locale/ru.py +952 -0
  44. mapcan/lsp/__init__.py +1 -0
  45. mapcan/lsp/analysis.py +865 -0
  46. mapcan/lsp/edits.py +1213 -0
  47. mapcan/lsp/exports.py +57 -0
  48. mapcan/lsp/server.py +588 -0
  49. mapcan/model.py +802 -0
  50. mapcan/protocols.py +47 -0
  51. mapcan/resolve.py +118 -0
  52. mapcan/schema.py +37 -0
  53. mapcan/templates/document.html.j2 +207 -0
  54. mapcan/templates/document.md.j2 +151 -0
  55. mapcan/templates/pdf/gost-19.typ +188 -0
  56. mapcan/templates/pdf/gost-2.105.typ +242 -0
  57. mapcan/templates/pdf/gost-7.32.typ +196 -0
  58. mapcan/templates/pdf/lib.typ +332 -0
  59. mapcan/templates/pdf/mapcan.typ +117 -0
  60. mapcan/templates/xlsx_style.yaml +22 -0
  61. mapcan/units.py +61 -0
  62. mapcan/updates.py +85 -0
  63. mapcan/upgrade.py +159 -0
  64. mapcan/userconfig.py +98 -0
  65. mapcan/validate.py +853 -0
  66. mapcan/web/__init__.py +0 -0
  67. mapcan/web/app.py +1292 -0
  68. mapcan/web/examples.py +88 -0
  69. mapcan/web/newmap.py +160 -0
  70. mapcan/web/session.py +438 -0
  71. mapcan/web/static/OFL-Sora.txt +93 -0
  72. mapcan/web/static/assets/index-DRMUrO54.js +31 -0
  73. mapcan/web/static/assets/index-S8VlG2gr.css +1 -0
  74. mapcan/web/static/favicon.svg +7 -0
  75. mapcan/web/static/index.html +14 -0
  76. mapcan/web/yamlops.py +768 -0
  77. mapcan-0.21.0.dist-info/METADATA +492 -0
  78. mapcan-0.21.0.dist-info/RECORD +85 -0
  79. mapcan-0.21.0.dist-info/WHEEL +4 -0
  80. mapcan-0.21.0.dist-info/entry_points.txt +5 -0
  81. mapcan-0.21.0.dist-info/licenses/LICENSE +176 -0
  82. mapcan-0.21.0.dist-info/licenses/NOTICE +15 -0
  83. mapcan-0.21.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +89 -0
  84. mapcan-0.21.0.dist-info/licenses/src/mapcan/fonts/OFL-PTSerif.txt +93 -0
  85. 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