mapcan 0.23.0__tar.gz → 0.23.2__tar.gz
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-0.23.0 → mapcan-0.23.2}/PKG-INFO +1 -1
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/format.md +19 -3
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/web.md +9 -5
- {mapcan-0.23.0 → mapcan-0.23.2}/examples/minimal.yaml +1 -2
- {mapcan-0.23.0 → mapcan-0.23.2}/pyproject.toml +1 -1
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/__init__.py +1 -1
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/cli.py +6 -1
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fmt.py +28 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/launcher.py +1 -1
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/locale/ru.py +25 -1
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/lsp/analysis.py +50 -15
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/lsp/edits.py +65 -14
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/lsp/server.py +43 -1
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/validate.py +63 -14
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/web/app.py +26 -6
- mapcan-0.23.2/src/mapcan/web/devicefile.py +113 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/web/session.py +154 -43
- mapcan-0.23.2/src/mapcan/web/static/assets/index-B4ld-hyP.js +35 -0
- mapcan-0.23.2/src/mapcan/web/static/assets/index-qvZIhvUc.css +1 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/web/static/index.html +2 -2
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/web/yamlops.py +120 -11
- mapcan-0.23.2/tests/test_device_file.py +258 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_eds.py +1 -1
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_fmt.py +9 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_include.py +28 -2
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_lsp.py +17 -1
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_lsp_edits.py +35 -4
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_validate.py +52 -1
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_web.py +12 -2
- mapcan-0.23.2/tests/test_yamlops.py +128 -0
- mapcan-0.23.0/src/mapcan/web/static/assets/index-BTf8uc75.js +0 -31
- mapcan-0.23.0/src/mapcan/web/static/assets/index-S8VlG2gr.css +0 -1
- mapcan-0.23.0/tests/test_yamlops.py +0 -71
- {mapcan-0.23.0 → mapcan-0.23.2}/.gitignore +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/LICENSE +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/NOTICE +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/README.md +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/THIRD_PARTY_NOTICES.md +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/ci-example.md +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/cli.md +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/format-changelog.md +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/demo-dark.gif +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/demo-light.gif +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/document-pages-dark.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/document-pages-light.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/vscode-demo-dark.gif +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/vscode-demo-light.gif +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/vscode-hover-dark.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/vscode-hover-light.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/vscode-map-dark.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/vscode-map-light.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/vscode-problems-dark.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/vscode-problems-light.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/vscode-quickfix-dark.gif +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/vscode-quickfix-light.gif +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/web-frame-dark.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/images/web-frame-light.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/logo-dark.svg +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/logo-light.svg +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/templates.md +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/docs/zephyr.md +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/examples/canopen-node.yaml +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/examples/j1939.yaml +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/examples/tractor/powertrain.yaml +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/examples/tractor/tractor.yaml +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/examples/vehicle-can.yaml +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/examples/zephyr/CMakeLists.txt +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/examples/zephyr/prj.conf +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/examples/zephyr/src/main.c +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/schema/mapcan-part.schema.json +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/schema/mapcan.schema.json +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/bits.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/canopen.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/codegen/__init__.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/codegen/c.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/codegen/common.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/codegen/py.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/codegen/zephyr.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/diagnostics.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/diff.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/docmodel.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/export/__init__.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/export/dbc.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/export/eds.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/export/pdf.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/export/text.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/export/xlsx.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/extras.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fonts/OFL-PTSerif.txt +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fonts/OFL.txt +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fonts/PT_Sans-Web-Bold.ttf +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fonts/PT_Sans-Web-BoldItalic.ttf +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fonts/PT_Sans-Web-Italic.ttf +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fonts/PT_Sans-Web-Regular.ttf +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fonts/PT_Serif-Web-Bold.ttf +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fonts/PT_Serif-Web-BoldItalic.ttf +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fonts/PT_Serif-Web-Italic.ttf +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/fonts/PT_Serif-Web-Regular.ttf +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/i18n.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/importers/__init__.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/importers/dbc.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/layout.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/loader.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/locale/__init__.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/lsp/__init__.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/lsp/exports.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/model.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/protocols.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/resolve.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/schema.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/templates/document.html.j2 +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/templates/document.md.j2 +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/templates/pdf/gost-19.typ +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/templates/pdf/gost-2.105.typ +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/templates/pdf/gost-7.32.typ +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/templates/pdf/lib.typ +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/templates/pdf/mapcan.typ +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/templates/xlsx_style.yaml +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/units.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/updates.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/upgrade.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/userconfig.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/web/__init__.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/web/examples.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/web/newmap.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/web/static/OFL-Sora.txt +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/src/mapcan/web/static/favicon.svg +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/__init__.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/conftest.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/data/invalid_map.yaml +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/data/legacy.yaml +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/data/logo.png +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/data/logo.svg +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/data/reference.yaml +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/data/zephyr_stub/zephyr/drivers/can.h +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/data/zephyr_stub/zephyr/kernel.h +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/golden/reference.md +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/golden/reference_bus1.h +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_auth.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_cli.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_codegen.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_diff.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_direction.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_exchange.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_export.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_extras.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_i18n.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_import.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_j1939.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_launcher.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_loader_fast.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_model.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_new_signal.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_source.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_templates.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_titles.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_units.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_upgrade.py +0 -0
- {mapcan-0.23.0 → mapcan-0.23.2}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mapcan
|
|
3
|
-
Version: 0.23.
|
|
3
|
+
Version: 0.23.2
|
|
4
4
|
Summary: A YAML map of CAN signals as the single source of truth: validation, Excel/PDF/Markdown/HTML, DBC, C/Python code
|
|
5
5
|
Project-URL: Homepage, https://github.com/ordu1453/mapcan
|
|
6
6
|
Project-URL: Documentation, https://github.com/ordu1453/mapcan/blob/main/docs/format.md
|
|
@@ -180,7 +180,7 @@ CANopen ID computation:
|
|
|
180
180
|
| `terminator` | `string`: the byte that ends a shorter text and fills the rest of the field: a number (`0x00`) or one character (`"*"`, as in J1939 text parameters). Without it the text fills the field and unused bytes are 0x00 |
|
|
181
181
|
| `start_bit` + `bit_length` | Canonical position |
|
|
182
182
|
| `byte` [+ `bit`] + `byte_length` \| `bit_length` | Shorthand position (see below) |
|
|
183
|
-
| `byte_order` | `little_endian` (default) or `big_endian` |
|
|
183
|
+
| `byte_order` | `little_endian` (default) or `big_endian`. CANopen and J1939 carry little-endian data, so the editors offer the field only on other buses (or when it is set), and validation warns about a big-endian signal there (`byte-order-protocol`) |
|
|
184
184
|
| `representation` | How the raw value is read as a number: `unsigned` (default), `twos_complement` (the usual signed integer), `ones_complement` or `sign_magnitude`. Generated C and Python code packs and unpacks the value in this representation; the last two have a symmetric range (−127…127 for 8 bits). DBC stores two's complement only, so the export warns about the other two. Formerly `signed` + `signed_code` |
|
|
185
185
|
| `values` | Value table: `{0: NO_DATA, 1: {name: OK, description: "normal"}}` |
|
|
186
186
|
| `value_format` | `dec` \| `hex`: how values are shown in documents |
|
|
@@ -302,6 +302,13 @@ marked as float.
|
|
|
302
302
|
`start_bit` cannot be combined with `byte`/`bit`, and `bit_length` cannot be combined with
|
|
303
303
|
`byte_length`.
|
|
304
304
|
|
|
305
|
+
Both notations are valid. The editors (the forms and bit layouts of the web editor and VS Code,
|
|
306
|
+
`mapcan import dbc`) write one of them, whichever field or drag changed the position: the shorthand
|
|
307
|
+
when the signal takes whole bytes from a byte boundary (`byte` + `byte_length`) or fits in one byte
|
|
308
|
+
(`byte` + `bit` + `bit_length`), and `start_bit` + `bit_length` otherwise. A length the encoding
|
|
309
|
+
implies (32 bits of a `float`, 1 bit of a `flag`) and `bit: 0` stay omitted where they were.
|
|
310
|
+
`mapcan fmt --check` notes the positions written otherwise, without failing.
|
|
311
|
+
|
|
305
312
|
#### Big-endian
|
|
306
313
|
|
|
307
314
|
For `big_endian` (Motorola) signals, `start_bit` is also the position of the least significant
|
|
@@ -567,6 +574,13 @@ file (or an earlier included file) already describes can only get more `devices`
|
|
|
567
574
|
its other fields, and a value table name used twice, are errors (`include`). Included files are
|
|
568
575
|
not nested.
|
|
569
576
|
|
|
577
|
+
To keep one file per device, add a device with **Add Device in a New File…** on a bus in the
|
|
578
|
+
VS Code Map view, or **+ Device in a new file** on a bus page of the web editor: the editor makes
|
|
579
|
+
the file (by default `devices/<name>.yaml`) with the device, named after the file when that name
|
|
580
|
+
is a free identifier on the bus, and adds it to `include`, in one edit. Deleting a device from the
|
|
581
|
+
editors' trees deletes an included file it leaves adding nothing to the map (only the bus by name,
|
|
582
|
+
without devices, frames or value tables), with its line of `include`; Undo brings both back.
|
|
583
|
+
|
|
570
584
|
Validation, documents and generated code use the whole map. Messages about an included file
|
|
571
585
|
point into that file on the command line; in the editors they are shown on the `include` line
|
|
572
586
|
of the main file, with the file and line in the text. The editors' trees show the included buses,
|
|
@@ -610,7 +624,8 @@ Included devices can be chosen as senders and receivers of the main file's frame
|
|
|
610
624
|
The same map can be written with the keys in any order. `mapcan fmt FILE…` writes the keys of every
|
|
611
625
|
node in one order, so that diffs of maps edited by different people and editors show only what changed;
|
|
612
626
|
`mapcan fmt --check` exits 1 if a file is not in that order (for CI and a pre-commit hook). Included
|
|
613
|
-
files are formatted with their map.
|
|
627
|
+
files are formatted with their map. `--check` also notes signal positions written in another notation
|
|
628
|
+
than the editors write (see [Shorthand position](#shorthand-position)); they do not fail the check.
|
|
614
629
|
|
|
615
630
|
Only the order changes. Comments move with their key (a comment line just above a key belongs to it),
|
|
616
631
|
and the author's choices stay: block or flow style (`{name: …}`), the position notation (`start_bit`
|
|
@@ -684,6 +699,7 @@ turned off with `--disable CODE` or `lint.disable`.
|
|
|
684
699
|
| `dlc-missing` | error | Frame has no `length` (only CANopen service frames may omit it) |
|
|
685
700
|
| `canopen-dlc` | warning | Length of a CANopen service frame does not match CiA 301 |
|
|
686
701
|
| `frame-type-protocol` | warning | `canopen` set on a frame of a non-CANopen bus |
|
|
702
|
+
| `byte-order-protocol` | warning | A big-endian signal spanning bytes on a CANopen or J1939 bus: both carry little-endian data (CiA 301, SAE J1939-71) |
|
|
687
703
|
| `direction-deprecated` | warning | `direction` is set: it follows from the CANopen function and `sender`; a frame from outside the map goes to the frames of its bus. `mapcan migrate` does both |
|
|
688
704
|
| `direction-mismatch` | warning | Direction conflicts with the frame's CANopen function |
|
|
689
705
|
|
|
@@ -697,7 +713,7 @@ turned off with `--disable CODE` or `lint.disable`.
|
|
|
697
713
|
| `j1939-field-protocol` | warning | pgn/priority/destination/address/spn on a non-J1939 bus |
|
|
698
714
|
| `j1939-spn-duplicate` | warning | The same SPN on several signals of the bus |
|
|
699
715
|
| `j1939-tp` | info | A PGN longer than 8 bytes is sent with the J1939-21 transport protocol |
|
|
700
|
-
| `canopen-od` | warning | Dictionary object on a signal outside a device's PDO, outside 0x2000..0x9FFF
|
|
716
|
+
| `canopen-od` | warning | Dictionary object on a signal outside a device's PDO, or outside 0x2000..0x9FFF |
|
|
701
717
|
| `od-duplicate` | warning | The same dictionary object on several signals of a device |
|
|
702
718
|
|
|
703
719
|
### Sender and receivers
|
|
@@ -49,6 +49,8 @@ offers to save them before switching.
|
|
|
49
49
|
|
|
50
50
|
The start screen also lists the bundled examples: pick one, choose where to save a copy, and it
|
|
51
51
|
opens. The mapcan version is shown in the top bar next to the title and on the start screen.
|
|
52
|
+
**File → Report an issue** (and the link on the start screen) opens a new GitHub issue with the
|
|
53
|
+
versions of mapcan, the server's system and the browser filled in; the map itself is not sent.
|
|
52
54
|
|
|
53
55
|
### Security
|
|
54
56
|
|
|
@@ -110,13 +112,14 @@ The profile records who made which changes:
|
|
|
110
112
|
|---|---|
|
|
111
113
|
| **Tree** (left) | Document → buses → devices → frames → signals → flags and SDO exchanges. Nodes are labelled with their human-readable names (`title`), not identifiers: a device shows its abbreviation, if set, and its full name (`BMS · Battery management system`); a frame or signal without a name falls back to its identifier. Double-click a name (or a device abbreviation) to edit it. Buses and devices must have a full name: a new one gets a default name, and its name field opens in the tree right away so you can type the real one; a map without it reports `missing-title`. Frames show their CAN ID, signals the bits they occupy. A dot marks an error or warning in the node itself, a ring marks one in its children. Under a device, frames of other devices that it receives follow its own frames as links (↪, in italics), labelled with the sender (its short name if set) and the frame name; clicking one opens the original frame. **Delete** on such a link removes the device from the frame's receivers and keeps the frame itself. |
|
|
112
114
|
| **Search** | By name, description or CAN ID (`0x196` or `406`). With several words, all of them must match. |
|
|
113
|
-
| **Properties** | The form for the selected node. A value is applied on Enter or when the field loses focus. Validation messages appear under the field. The form shows only the fields that apply to the node: a frame on a CANopen bus hides **Direction** when its type sets it, an RX frame has no **Receivers** (the device itself receives it),
|
|
115
|
+
| **Properties** | The form for the selected node. A value is applied on Enter or when the field loses focus. Validation messages appear under the field. The form shows only the fields that apply to the node: a frame on a CANopen bus hides **Direction** when its type sets it, an RX frame has no **Receivers** (the device itself receives it), and **Receivers** appear only when there are other devices on the bus. There is no field for the sender: it follows from where the frame is written (its device, or outside the map for a frame of the bus); the master that sends a CANopen RX function written at its node stays `sender` in the YAML. A CANopen service frame (NMT, Heartbeat, TIME, SDO) has no **Length** field: its length comes from its CANopen function. A field that is set in the YAML or has a validation message stays visible so it can be fixed. Elsewhere in the editor (page headings, breadcrumbs, frame, device and bus lists, sender and receivers) objects are shown by their names, with the identifier next to them in grey. Text fields (names, descriptions) are edited in the document language; translations into the other language written in the YAML are kept. The **?** next to a field label explains the setting, why it matters and which values are allowed (shown on hover; click to pin, Esc to close). |
|
|
114
116
|
| **Frame layout** | The bit map of the frame: rows are bytes, columns are bits 7…0. Cells are labelled with signal names and the flags of bit fields. |
|
|
115
117
|
| **Frames** | The frame list on a bus or device page; ✕ in a row deletes the frame. **+ Frame** adds a frame to the list without changing the selected node. |
|
|
116
|
-
| **
|
|
118
|
+
| **Devices** | The device list on a bus page. **+ Device** adds a device to the file that describes the bus. **+ Device in a new file** asks for a file, `devices/<name>.yaml` relative to the map by default, makes it with the device (`mapcan: 1`, `part_of` and the bus by name; the device is named after the file when that name is a free identifier on the bus) and adds it to `include` of the map: one undo step, and **Save** writes both files. A map edited by several people can keep one file per device this way, so changes to different devices do not meet in one file. Deleting such a device deletes its file, left empty, and its line of `include` (on Save; Ctrl+Z brings them back). |
|
|
119
|
+
| **Receives** | On a device page: the bus frames the device receives (its own RX frames and other devices' frames that list it as a receiver), with their senders. Below it, **Transmits frames of other devices** lists the frames it sends on behalf of other devices (`sender`). Both are computed from the frames; click a row to jump to the frame. On a frame page, **Receivers** are toggled on and off; renaming a device updates them and `sender`. ✕ in a row of another device's frame removes the device from its receivers. On an RX frame, **Frame of another device** picks a frame that is already described at its sender: the device is added to that frame's receivers and its own copy is removed (Ctrl+Z undoes it). The `rx-duplicate` message on such a frame has a button that does the same. |
|
|
117
120
|
| **Values / Flags** | Enum and bit field tables: value (decimal or `0x…`), name, description. **+ Flag** adds a row without leaving the signal page. |
|
|
118
121
|
| **Validation** (bottom) | The same rules as `mapcan validate`. Click a message to jump to the node (or, on the YAML tab, to the line). While the YAML tab holds text that is not applied yet (for example, with a syntax error), the panel lists the messages for that text and marks them **YAML text not applied**. |
|
|
119
|
-
| **YAML** | Edit the whole file as text. The text is validated as you type: lines with problems are marked in the gutter (hover a line number to read the message), and the first syntax error is shown in the YAML toolbar; click it to jump to its line. Once the text parses, it is applied automatically after a short pause, so the tree and the editor follow your typing; a whole run of typing is one undo step. Switching to the editor applies what is left (it stays on the YAML tab if there is a syntax error). Renaming a device in the text also updates `sender` and `receivers` of the bus frames, as renaming in the form does. Tab and Shift+Tab indent and outdent the selected lines, Enter keeps the indentation (one level deeper after `key:`), Backspace after spaces goes back to the previous tab stop (2 spaces); press Esc first to move focus with Tab. Ctrl+Enter applies the edit at once. While you type a key, a list offers the fields allowed at that place that are not set yet (required ones first), with the field description from the format schema; after `key:` it offers the values of enumerations (`encoding`, `protocol`, `byte_order`, `true`/`false`…). The same works inside a one-line `{...}` mapping (after `{` and after `, `), and a new `- ` list item also offers `{ }` to write the item on one line. Arrows choose, Enter or Tab inserts (a list or object field also opens its first level), Esc closes, Ctrl+Space shows the list on an empty line. |
|
|
122
|
+
| **YAML** | Edit the whole file as text. The text is validated as you type: lines with problems are marked in the gutter (hover a line number to read the message), and the first syntax error is shown in the YAML toolbar; click it to jump to its line. Once the text parses, it is applied automatically after a short pause, so the tree and the editor follow your typing; a whole run of typing is one undo step. Switching to the editor applies what is left (it stays on the YAML tab if there is a syntax error). Renaming a device in the text also updates `sender` and `receivers` of the bus frames, as renaming in the form does. Tab and Shift+Tab indent and outdent the selected lines, Enter keeps the indentation (one level deeper after `key:`), Backspace after spaces goes back to the previous tab stop (2 spaces); press Esc first to move focus with Tab. Ctrl+Enter applies the edit at once. While you type a key, a list offers the fields allowed at that place that are not set yet (required ones first), with the field description from the format schema; after `key:` it offers the values of enumerations (`encoding`, `protocol`, `byte_order`, `true`/`false`…). The same works inside a one-line `{...}` mapping (after `{` and after `, `), and a new `- ` list item also offers `{ }` to write the item on one line. Arrows choose, Enter or Tab inserts (a list or object field also opens its first level), Esc closes, Ctrl+Space shows the list on an empty line. A map with included files (`include`) shows one file at a time: a node selected in the tree opens the file it is written in (a device of `devices/node.yaml`, a frame where its sender describes it), with the node's lines highlighted, and the list in the YAML toolbar switches between the map and its files (• marks a file with unsaved edits). The text of an included file is edited and applied the same way and goes to that file; **Save** writes it. On the editor tab, **Open the YAML of …** in the note above a node of an included file opens it. |
|
|
120
123
|
| **Export** | Documents (PDF, HTML, Markdown, Excel) and development formats (DBC, C, Python), generated from the current state, including unsaved changes. Clicking a document opens a preview; for PDF, the template is chosen there: `mapcan` or ESKD (see [templates.md](templates.md)). ↓ on the right downloads the file directly. The document language (English or Russian; the map's language by default) is chosen in the menu and in the preview. The ESKD template is always in Russian. |
|
|
121
124
|
| **Layout and document details** | In the document properties: logo (file upload), designation, letter, organization, city, title block signatures, approval blocks. |
|
|
122
125
|
| **Git → Compare versions…** | Diffs the current map against a git revision (HEAD, a tag or a commit) or an uploaded file: changes to frames, signals, values and flags. **Add to history** writes the changelog into `document.history` and bumps the version. |
|
|
@@ -177,8 +180,9 @@ available at `http://127.0.0.1:8765/api/docs`.
|
|
|
177
180
|
| POST | `/api/file/open`, `/api/file/new`, `/api/file/example`, `/api/file/save_as`, `/api/file/close` | File operations; `409 unsaved` means there are unsaved changes (pass `force: true` to proceed) |
|
|
178
181
|
| GET | `/api/state` | Text, data, computed layout and diagnostics |
|
|
179
182
|
| POST | `/api/ops` | `{"ops": [...]}`: apply operations atomically |
|
|
180
|
-
| POST | `/api/
|
|
181
|
-
| POST | `/api/
|
|
183
|
+
| POST | `/api/device_file` | `{"bus": ["buses", 0], "device": {...}, "file": "devices/node.yaml"}`: a new device in a new file, added to `include` |
|
|
184
|
+
| POST | `/api/text` | `{"text": ..., "file": null}`: replace the YAML text of the map, or of an included file (`file`, relative to the map) |
|
|
185
|
+
| POST | `/api/check` | `{"text": ..., "file": null}`: validate text without applying it; returns `{diagnostics}` (a message about an included file has `part`: its file, line and column) |
|
|
182
186
|
| POST | `/api/undo`, `/api/redo` | Edit history |
|
|
183
187
|
| POST | `/api/save` | `{"force": false}`; `409` means the file changed on disk |
|
|
184
188
|
| POST | `/api/reload` | Reload the file from disk |
|
|
@@ -66,12 +66,11 @@ buses:
|
|
|
66
66
|
offset: -20
|
|
67
67
|
unit: °C
|
|
68
68
|
|
|
69
|
-
- name: speed #
|
|
69
|
+
- name: speed # two bytes, little-endian (CANopen): byte 5 is the low byte
|
|
70
70
|
description: Speed
|
|
71
71
|
encoding: scaled
|
|
72
72
|
byte: 5
|
|
73
73
|
byte_length: 2
|
|
74
|
-
byte_order: big_endian
|
|
75
74
|
unit: rpm
|
|
76
75
|
|
|
77
76
|
- name: counter # arbitrary position: bits 56..59
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mapcan"
|
|
3
|
-
version = "0.23.
|
|
3
|
+
version = "0.23.2"
|
|
4
4
|
description = "A YAML map of CAN signals as the single source of truth: validation, Excel/PDF/Markdown/HTML, DBC, C/Python code"
|
|
5
5
|
# readme: built from README.md with absolute links for PyPI (see fancy-pypi-readme below)
|
|
6
6
|
dynamic = ["readme"]
|
|
@@ -261,7 +261,7 @@ def fmt(
|
|
|
261
261
|
paths: list[Path] = typer.Argument(..., exists=True, dir_okay=False, help=_("Map files.")),
|
|
262
262
|
check: bool = typer.Option(False, "--check", help=_("Do not write; exit code 1 if a file is not formatted.")),
|
|
263
263
|
) -> None:
|
|
264
|
-
from .fmt import FmtError, format_text
|
|
264
|
+
from .fmt import FmtError, format_text, position_notes
|
|
265
265
|
|
|
266
266
|
unformatted, failed = [], False
|
|
267
267
|
for f in _with_included(paths):
|
|
@@ -272,6 +272,11 @@ def fmt(
|
|
|
272
272
|
typer.secho(f"{f}: {e}", fg=typer.colors.RED, err=True)
|
|
273
273
|
failed = True
|
|
274
274
|
continue
|
|
275
|
+
if check:
|
|
276
|
+
# Positions in another notation than the editors write (#117): a note, the map stays valid
|
|
277
|
+
for line, name, keys in position_notes(text):
|
|
278
|
+
typer.echo(_("{file}:{line}: note: signal {name!r}: the editors write its position as {keys}",
|
|
279
|
+
file=f, line=line, name=name, keys=keys))
|
|
275
280
|
if new == text:
|
|
276
281
|
continue
|
|
277
282
|
unformatted.append(f)
|
|
@@ -315,3 +315,31 @@ def format_text(text: str) -> str:
|
|
|
315
315
|
or new.count("#") != text.count("#")):
|
|
316
316
|
raise FmtError(_("cannot format: the result would read differently; the file is left as it is"))
|
|
317
317
|
return new
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def position_notes(text: str) -> list[tuple[int, str, str]]:
|
|
321
|
+
"""Signals whose position is not written in the canonical notation the editors use (#117): (line from 1, signal,
|
|
322
|
+
the canonical keys as YAML). ``mapcan fmt`` does not rewrite them: they are reported by ``--check`` only."""
|
|
323
|
+
from .web.yamlops import canonical_position
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
doc = make_yaml(text).load(text)
|
|
327
|
+
except Exception: # noqa: BLE001 — not YAML: format_text reports it
|
|
328
|
+
return []
|
|
329
|
+
out = []
|
|
330
|
+
|
|
331
|
+
def frames(node: Any) -> list:
|
|
332
|
+
items = node.get("frames") if isinstance(node, CommentedMap) else None
|
|
333
|
+
return items if isinstance(items, CommentedSeq) else []
|
|
334
|
+
|
|
335
|
+
buses = doc.get("buses") if isinstance(doc, CommentedMap) else None
|
|
336
|
+
for bus in buses if isinstance(buses, CommentedSeq) else []:
|
|
337
|
+
devices = bus.get("devices") if isinstance(bus, CommentedMap) else None
|
|
338
|
+
owners = [bus, *(devices if isinstance(devices, CommentedSeq) else [])]
|
|
339
|
+
for frame in (f for o in owners for f in frames(o)):
|
|
340
|
+
signals = frame.get("signals") if isinstance(frame, CommentedMap) else None
|
|
341
|
+
for sig in signals if isinstance(signals, CommentedSeq) else []:
|
|
342
|
+
if (keys := canonical_position(sig)) is not None:
|
|
343
|
+
written = ", ".join(f"{k}: {v}" for k, v in keys.items())
|
|
344
|
+
out.append((sig.lc.line + 1, str(sig.get("name", "?")), written))
|
|
345
|
+
return out
|
|
@@ -252,7 +252,7 @@ def run(path: Path | None = None, *, window: bool = True, readonly: bool = False
|
|
|
252
252
|
|
|
253
253
|
app = create_app(session, cwd=cwd.resolve(), readonly=readonly, check_updates=check_updates,
|
|
254
254
|
instance_token=token if single_instance else None, on_activate=on_activate,
|
|
255
|
-
allowed_hosts=LOCAL_HOSTS)
|
|
255
|
+
allowed_hosts=LOCAL_HOSTS, mode="app")
|
|
256
256
|
server = ServerThread(app, host, port)
|
|
257
257
|
try:
|
|
258
258
|
server.start()
|
|
@@ -234,6 +234,8 @@ RU: dict[str, str] = {
|
|
|
234
234
|
"direction кадра {name!r} противоречит его функции CANopen или отправителю: удалите его и укажите в sender "
|
|
235
235
|
"устройство, которое передаёт кадр",
|
|
236
236
|
"CANopen frame type on a non-CANopen bus": "Тип кадра CANopen на шине не CANopen",
|
|
237
|
+
"Big-endian signal on a CANopen or J1939 bus (they carry little-endian data)":
|
|
238
|
+
"Сигнал big-endian на шине CANopen или J1939 (они передают данные little-endian)",
|
|
237
239
|
"sdo description on a frame that is not sdo_tx/sdo_rx": "Описание sdo у кадра не типа sdo_tx/sdo_rx",
|
|
238
240
|
"Duplicate SDO exchange (command, index, subindex)": "Повтор SDO-обмена (команда, индекс, субиндекс)",
|
|
239
241
|
"Name does not match the style rule (lint)": "Имя не соответствует правилу стиля (lint)",
|
|
@@ -356,6 +358,16 @@ RU: dict[str, str] = {
|
|
|
356
358
|
"short label {short!r} is already used by device {other!r}": "обозначение {short!r} уже у устройства {other!r}",
|
|
357
359
|
"frame {name!r} differs from {other!r} only in case: their C macros will collide":
|
|
358
360
|
"кадр {name!r} отличается от {other!r} только регистром: в C-коде их макросы совпадут",
|
|
361
|
+
"Set the bitrate of the bus to estimate its load.": "Задайте скорость шины (bitrate), чтобы оценить её загрузку.",
|
|
362
|
+
"Set data_bitrate to estimate the load: frames with bit rate switching send their data at it.":
|
|
363
|
+
"Задайте data_bitrate, чтобы оценить загрузку: кадры с переключением скорости передают данные на ней.",
|
|
364
|
+
"Bus load unknown: no frame has a cycle or a minimum interval.":
|
|
365
|
+
"Загрузка неизвестна: ни у одного кадра нет периода или минимального интервала.",
|
|
366
|
+
"Estimated bus load: {load}": "Расчётная загрузка шины: {load}",
|
|
367
|
+
"At least: {counted} of {total} frames are counted; frames without a cycle or a minimum interval are not.":
|
|
368
|
+
"Не меньше: учтено кадров {counted} из {total}; кадры без периода и минимального интервала не учитываются.",
|
|
369
|
+
"Frames sent on an event are counted at their minimum interval, with worst-case bit stuffing.":
|
|
370
|
+
"Кадры по событию учитываются с минимальным интервалом, bit stuffing — по худшему случаю.",
|
|
359
371
|
"estimated load of bus {bus!r} is {load:.0f}% (frames counted: {count}, {bitrate} bit/s; event frames at their "
|
|
360
372
|
"minimum interval); keep it at or below {limit}%":
|
|
361
373
|
"расчётная загрузка шины {bus!r} — {load:.0f} % (учтено кадров: {count}, {bitrate} бит/с; кадры по событию — "
|
|
@@ -470,7 +482,7 @@ RU: dict[str, str] = {
|
|
|
470
482
|
"{name!r}: subindex without index": "{name!r}: subindex без index",
|
|
471
483
|
"{name!r}: index {index:#06x} is outside the manufacturer and profile areas (0x2000..0x9FFF)":
|
|
472
484
|
"{name!r}: индекс {index:#06x} вне областей производителя и профилей (0x2000..0x9FFF)",
|
|
473
|
-
"{name!r}:
|
|
485
|
+
"{name!r}: {protocol} carries little-endian data": "{name!r}: {protocol} передаёт данные little-endian",
|
|
474
486
|
"object {index:04X}:{sub:02X} is already mapped by {other}": "объект {index:04X}:{sub:02X} уже у сигнала {other}",
|
|
475
487
|
"CANopen: index of the dictionary object mapped into the PDO.":
|
|
476
488
|
"CANopen: индекс объекта словаря, отображённого в PDO.",
|
|
@@ -602,6 +614,12 @@ RU: dict[str, str] = {
|
|
|
602
614
|
"could not save settings: {error}": "не удалось сохранить настройки: {error}",
|
|
603
615
|
"file not found: {path}": "файл не найден: {path}",
|
|
604
616
|
"file already exists: {path}": "файл уже существует: {path}",
|
|
617
|
+
"Enter the name of the file": "Введите имя файла",
|
|
618
|
+
"the name of an included file must end with .yaml or .yml":
|
|
619
|
+
"имя включаемого файла должно оканчиваться на .yaml или .yml",
|
|
620
|
+
"the path of an included file is relative to the map": "путь включаемого файла указывается относительно карты",
|
|
621
|
+
"include of the map is not a list": "include карты — не список",
|
|
622
|
+
"Save the map to a file first.": "Сначала сохраните карту в файл.",
|
|
605
623
|
"{fmt} export error: {error}": "ошибка экспорта {fmt}: {error}",
|
|
606
624
|
"the file is not UTF-8 encoded": "файл не в UTF-8",
|
|
607
625
|
"could not read {name}: {error}": "не удалось прочитать {name}: {error}",
|
|
@@ -672,6 +690,10 @@ RU: dict[str, str] = {
|
|
|
672
690
|
"Bit": "Бит",
|
|
673
691
|
"Flag": "Флаг",
|
|
674
692
|
"Byte": "Байт",
|
|
693
|
+
"Signal": "Сигнал",
|
|
694
|
+
"Position": "Положение",
|
|
695
|
+
"Size": "Размер",
|
|
696
|
+
"bit": "бит",
|
|
675
697
|
"Export canceled: the map has errors ({errors}). Fix them and try again.":
|
|
676
698
|
"Экспорт отменён: в карте есть ошибки ({errors}). Исправьте их и повторите.",
|
|
677
699
|
"Save the map to a file before exporting.": "Сохраните карту в файл перед экспортом.",
|
|
@@ -866,6 +888,8 @@ RU: dict[str, str] = {
|
|
|
866
888
|
"Do not write; exit code 1 if a file is not formatted.":
|
|
867
889
|
"Не записывать; код выхода 1, если файл не отформатирован.",
|
|
868
890
|
"{file}: not formatted, run mapcan fmt": "{file}: не отформатирован, выполните mapcan fmt",
|
|
891
|
+
"{file}:{line}: note: signal {name!r}: the editors write its position as {keys}":
|
|
892
|
+
"{file}:{line}: примечание: сигнал {name!r}: редакторы записывают его положение как {keys}",
|
|
869
893
|
"part_of: the map {map!r} is not found": "part_of: карта {map!r} не найдена",
|
|
870
894
|
"part_of: the map {map!r} does not include this file": "part_of: карта {map!r} не включает этот файл",
|
|
871
895
|
"the map {map!r} has {n} error(s) outside this file: open it to see them":
|
|
@@ -21,7 +21,7 @@ from ..docmodel import fmt_num
|
|
|
21
21
|
from ..i18n import _, normalize_language
|
|
22
22
|
from ..loader import PART_OF_RE, LoadResult, SourceMap, loads, paused_gc
|
|
23
23
|
from ..model import MapCan, enum_entries
|
|
24
|
-
from ..validate import bus_load_text, check
|
|
24
|
+
from ..validate import bus_load_note, bus_load_text, check
|
|
25
25
|
|
|
26
26
|
Pos = tuple[int, int] # 0-based (line, column)
|
|
27
27
|
Path_ = tuple
|
|
@@ -446,7 +446,8 @@ def device_label(d: dict, lang: str) -> str:
|
|
|
446
446
|
|
|
447
447
|
|
|
448
448
|
def map_tree(a: Analysis) -> list[dict]:
|
|
449
|
-
"""Nodes: kind, label, ident, description, dir (frames), from (received frames),
|
|
449
|
+
"""Nodes: kind, label, ident, description, dir (frames), from (received frames), to (frames the device sends,
|
|
450
|
+
described at their receiver), range, selection, children.
|
|
450
451
|
|
|
451
452
|
Labels are as in the web editor's tree: titles in the document language, the identifier (``ident``)
|
|
452
453
|
only if there is no title. Built from the raw YAML; computed IDs and bits come from the model if the map parses.
|
|
@@ -484,7 +485,7 @@ def map_tree(a: Analysis) -> list[dict]:
|
|
|
484
485
|
def model(path: Path_):
|
|
485
486
|
return _find(a.map, path) if a.map is not None else None
|
|
486
487
|
|
|
487
|
-
def frame_node(f: dict, path: Path_) -> dict | None:
|
|
488
|
+
def frame_node(f: dict, path: Path_, devices: list) -> dict | None:
|
|
488
489
|
loc = rng(path)
|
|
489
490
|
if loc is None:
|
|
490
491
|
return None
|
|
@@ -494,7 +495,12 @@ def map_tree(a: Analysis) -> list[dict]:
|
|
|
494
495
|
sub = f"PGN {_hexw(f['pgn'], 4)}"
|
|
495
496
|
elif isinstance(rf, M.Frame) and rf.can_id is not None:
|
|
496
497
|
sub = _hex(rf.can_id, rf.extended)
|
|
497
|
-
|
|
498
|
+
label = display_name(f, lang)
|
|
499
|
+
# A frame described at its receiver (sender: another device): the label names the sender, as a link does
|
|
500
|
+
if isinstance(rf, M.Frame) and rf.device is not None and rf.sender_name not in (None, rf.device.name):
|
|
501
|
+
sender = next((x for x in devices if x.get("name") == rf.sender_name), None)
|
|
502
|
+
label = f"{device_label(sender, lang) if sender else rf.sender_name} · {label}"
|
|
503
|
+
node = {"kind": "frame", "label": label, "ident": name_of(f), "description": sub,
|
|
498
504
|
"dir": rf.direction if isinstance(rf, M.Frame) and rf.device is not None else None,
|
|
499
505
|
"children": [], **loc}
|
|
500
506
|
for si, s in enumerate(_items(f.get("signals"))):
|
|
@@ -528,13 +534,15 @@ def map_tree(a: Analysis) -> list[dict]:
|
|
|
528
534
|
if (bl := rng(bp)) is None:
|
|
529
535
|
continue
|
|
530
536
|
mbus = model(bp)
|
|
531
|
-
# The estimated bus load ("≥" when frames without a known period are not counted)
|
|
537
|
+
# The estimated bus load ("≥" when frames without a known period are not counted), and what it is
|
|
538
|
+
is_bus = isinstance(mbus, M.Bus)
|
|
532
539
|
bnode = {"kind": "bus", "label": display_name(bus, lang), "ident": name_of(bus),
|
|
533
|
-
"description": bus_load_text(mbus) if
|
|
540
|
+
"description": bus_load_text(mbus) if is_bus else "",
|
|
541
|
+
"load_note": bus_load_note(mbus) if is_bus else "", "children": [], **bl}
|
|
542
|
+
devices = _items(bus.get("devices"))
|
|
534
543
|
for fi, f in enumerate(_items(bus.get("frames"))):
|
|
535
|
-
if (fn := frame_node(f, bp + ("frames", fi))) is not None:
|
|
544
|
+
if (fn := frame_node(f, bp + ("frames", fi), devices)) is not None:
|
|
536
545
|
bnode["children"].append(fn)
|
|
537
|
-
devices = _items(bus.get("devices"))
|
|
538
546
|
for di, d in enumerate(devices):
|
|
539
547
|
dp = bp + ("devices", di)
|
|
540
548
|
if (dl := rng(dp)) is None:
|
|
@@ -544,7 +552,7 @@ def map_tree(a: Analysis) -> list[dict]:
|
|
|
544
552
|
dnode = {"kind": "device", "label": display_name(d, lang), "ident": name_of(d), "description": sub,
|
|
545
553
|
"children": [], **dl}
|
|
546
554
|
for fi, f in enumerate(_items(d.get("frames"))):
|
|
547
|
-
if (fn := frame_node(f, dp + ("frames", fi))) is not None:
|
|
555
|
+
if (fn := frame_node(f, dp + ("frames", fi), devices)) is not None:
|
|
548
556
|
dnode["children"].append(fn)
|
|
549
557
|
# Frames of other devices that the device receives: links after its own; the frame's title may
|
|
550
558
|
# not tell whose it is, so the link's label names the sender
|
|
@@ -561,6 +569,20 @@ def map_tree(a: Analysis) -> list[dict]:
|
|
|
561
569
|
"ident": rf.name, "dir": "rx", "from": sender_label or rf.sender_name or "",
|
|
562
570
|
"description": _hex(rf.can_id, rf.extended) if rf.can_id is not None else "",
|
|
563
571
|
"children": [], **loc})
|
|
572
|
+
# Frames the device sends that are described at their receiver (sender: this device): links too, the
|
|
573
|
+
# label names the device they are described at
|
|
574
|
+
for rf in mbus.all_frames() if isinstance(mbus, M.Bus) else []:
|
|
575
|
+
if (rf.device is None or rf.device.name == d.get("name") or rf.sender_name != d.get("name")
|
|
576
|
+
or (loc := rng(rf._path)) is None):
|
|
577
|
+
continue
|
|
578
|
+
owner = next((x for x in devices if x.get("name") == rf.device.name), None)
|
|
579
|
+
owner_label = device_label(owner, lang) if owner else rf.device.name
|
|
580
|
+
frame = raw_at_path(raw, rf._path)
|
|
581
|
+
title = display_name(frame if isinstance(frame, dict) else {"name": rf.name}, lang)
|
|
582
|
+
dnode["children"].append({
|
|
583
|
+
"kind": "frame", "label": f"{owner_label} · {title}", "ident": rf.name, "dir": "tx",
|
|
584
|
+
"to": owner_label, "description": _hex(rf.can_id, rf.extended) if rf.can_id is not None else "",
|
|
585
|
+
"children": [], **loc})
|
|
564
586
|
bnode["children"].append(dnode)
|
|
565
587
|
out.append(bnode)
|
|
566
588
|
return out
|
|
@@ -765,7 +787,12 @@ def frame_markdown(frame: M.Frame) -> str:
|
|
|
765
787
|
|
|
766
788
|
|
|
767
789
|
def frame_layout(frame: M.Frame) -> str:
|
|
768
|
-
"""
|
|
790
|
+
"""The signals of a frame and its bit map as markdown tables.
|
|
791
|
+
|
|
792
|
+
The list of signals comes first: a hover shows the top of a long text, and the numbers in the map mean nothing
|
|
793
|
+
without it. Rows of the map are bytes, columns are bits 7…0; consecutive bytes with the same owners are one row,
|
|
794
|
+
so a frame of 64 bytes stays short.
|
|
795
|
+
"""
|
|
769
796
|
sigs = frame.layout
|
|
770
797
|
if not sigs or frame.length == 0:
|
|
771
798
|
return ""
|
|
@@ -773,13 +800,21 @@ def frame_layout(frame: M.Frame) -> str:
|
|
|
773
800
|
for i, s in enumerate(sigs, 1):
|
|
774
801
|
for b in occupied_bits(s.start_bit or 0, s.bit_length or 1, s.byte_order == "big_endian"):
|
|
775
802
|
cells.setdefault(b, []).append(i)
|
|
803
|
+
legend = ["| # | " + _("Signal") + " | " + _("Position") + " | " + _("Size") + " |", "|---:|---|---|---:|"]
|
|
804
|
+
legend += [f"| {i} | `{s.name}` | {signal_bits(s)} | {s.bit_length or 1} " + _("bit") + " |"
|
|
805
|
+
for i, s in enumerate(sigs, 1)]
|
|
776
806
|
rows = ["| " + _("Byte") + " | " + " | ".join(str(b) for b in range(7, -1, -1)) + " |",
|
|
777
807
|
"|---:|" + ":-:|" * 8]
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
808
|
+
owners = [tuple(",".join(map(str, cells.get(byte * 8 + bit, []))) or "·" for bit in range(7, -1, -1))
|
|
809
|
+
for byte in range(frame.length)]
|
|
810
|
+
first = 0
|
|
811
|
+
for byte in range(1, frame.length + 1):
|
|
812
|
+
if byte < frame.length and owners[byte] == owners[first]:
|
|
813
|
+
continue
|
|
814
|
+
label = str(first) if byte - 1 == first else f"{first}–{byte - 1}"
|
|
815
|
+
rows.append(f"| {label} | " + " | ".join(owners[first]) + " |")
|
|
816
|
+
first = byte
|
|
817
|
+
return "\n".join(legend) + "\n\n" + "\n".join(rows)
|
|
783
818
|
|
|
784
819
|
|
|
785
820
|
# --------------------------------------------------------------------------- #
|
|
@@ -718,16 +718,8 @@ def add_node(a: Analysis, path: Path_, kind: str) -> TextChange | None:
|
|
|
718
718
|
("title", yaml_text(_default_title(a, "Шина", "Bus", buses))),
|
|
719
719
|
("protocol", "can")])
|
|
720
720
|
if kind == "device" and len(path) == 2:
|
|
721
|
-
|
|
722
|
-
fields = [("
|
|
723
|
-
("title", yaml_text(_default_title(a, "Устройство", "Device", devices)))]
|
|
724
|
-
protocol = (raw_at(a, path) or {}).get("protocol", "canopen")
|
|
725
|
-
if protocol == "canopen" and (nid := _free_number([d.get("node_id") for d in devices
|
|
726
|
-
if isinstance(d, dict)], 1, 127)):
|
|
727
|
-
fields.append(("node_id", f"0x{nid:02X}"))
|
|
728
|
-
elif protocol == "j1939" and (sa := _free_number([d.get("address") for d in devices
|
|
729
|
-
if isinstance(d, dict)], 0, 253)) is not None:
|
|
730
|
-
fields.append(("address", f"0x{sa:02X}"))
|
|
721
|
+
device = new_device(a, raw_at(a, path) or {}, raw_at(a, path + ("devices",)) or [])
|
|
722
|
+
fields = [(k, f"0x{v:02X}" if k in ("node_id", "address") else yaml_text(v)) for k, v in device.items()]
|
|
731
723
|
return append_item(a, path, "devices", fields)
|
|
732
724
|
if kind == "frame" and path and path[0] == "buses" and len(path) in (2, 4):
|
|
733
725
|
return append_item(a, path, "frames", _new_frame(a, path))
|
|
@@ -736,6 +728,51 @@ def add_node(a: Analysis, path: Path_, kind: str) -> TextChange | None:
|
|
|
736
728
|
return None
|
|
737
729
|
|
|
738
730
|
|
|
731
|
+
def new_device(a: Analysis, bus: dict, devices: list) -> dict:
|
|
732
|
+
"""Fields of a new device of a bus with these devices: a free name, title and CANopen node ID or J1939
|
|
733
|
+
address."""
|
|
734
|
+
out = {"name": _unique("device", _names(devices)),
|
|
735
|
+
"title": _default_title(a, "Устройство", "Device", devices)}
|
|
736
|
+
protocol = bus.get("protocol", "canopen")
|
|
737
|
+
if protocol == "canopen" and (nid := _free_number([d.get("node_id") for d in devices
|
|
738
|
+
if isinstance(d, dict)], 1, 127)):
|
|
739
|
+
out["node_id"] = nid
|
|
740
|
+
elif protocol == "j1939" and (sa := _free_number([d.get("address") for d in devices
|
|
741
|
+
if isinstance(d, dict)], 0, 253)) is not None:
|
|
742
|
+
out["address"] = sa
|
|
743
|
+
return out
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def device_file(a: Analysis, bus_path: Path_, file: str | None) -> dict:
|
|
747
|
+
"""A new device of a bus in a file of its own (#136). Without ``file``: {file} with the default name of the
|
|
748
|
+
file. With it: {device, target, text, ops}: the device, the new file and its text, and the operations that
|
|
749
|
+
add it to ``include`` of the map (paths of the map). ValueError: why it cannot be made."""
|
|
750
|
+
from ..web.devicefile import (
|
|
751
|
+
default_device_file,
|
|
752
|
+
device_file_target,
|
|
753
|
+
device_file_text,
|
|
754
|
+
include_ops,
|
|
755
|
+
named_after_file,
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
data = a.data if isinstance(a.data, dict) else {}
|
|
759
|
+
bus = raw_at_path(data, bus_path)
|
|
760
|
+
if len(bus_path) != 2 or not isinstance(bus, dict) or not isinstance(bus.get("name"), str):
|
|
761
|
+
raise ValueError(_("path not found: {path}", path=list(bus_path)))
|
|
762
|
+
main = a.result.source.file
|
|
763
|
+
if not main or not Path(main).is_file():
|
|
764
|
+
raise ValueError(_("Save the map to a file first."))
|
|
765
|
+
devices = [d for d in bus.get("devices") or [] if isinstance(d, dict)]
|
|
766
|
+
device = new_device(a, bus, devices)
|
|
767
|
+
if file is None:
|
|
768
|
+
return {"file": default_device_file(device["name"])}
|
|
769
|
+
target, line = device_file_target(Path(main), file)
|
|
770
|
+
device = named_after_file(device, target, _names(devices))
|
|
771
|
+
raw = a.result.raw if isinstance(a.result.raw, dict) else {}
|
|
772
|
+
return {"device": device, "target": target, "ops": include_ops(raw.get("include"), line),
|
|
773
|
+
"text": device_file_text(Path(main), target, bus["name"], {**device, "frames": []})}
|
|
774
|
+
|
|
775
|
+
|
|
739
776
|
class EditRefused(ValueError):
|
|
740
777
|
"""An edit that cannot be made; the message says why."""
|
|
741
778
|
|
|
@@ -801,10 +838,10 @@ def _add_signal(a: Analysis, path: Path_) -> TextChange | None:
|
|
|
801
838
|
start, length, grown = place
|
|
802
839
|
signals = raw_at(a, path + ("signals",)) or []
|
|
803
840
|
fields = [("name", _unique("signal", _names(signals))), ("encoding", "raw")]
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
841
|
+
# The position in the notation the other editors write (#117)
|
|
842
|
+
from ..web.yamlops import layout_keys
|
|
843
|
+
|
|
844
|
+
fields +=[(k, str(v)) for k, v in layout_keys(start, length, False, "raw").items()]
|
|
808
845
|
change = append_item(a, path, "signals", fields)
|
|
809
846
|
if change is None or grown is None:
|
|
810
847
|
return change
|
|
@@ -1208,6 +1245,8 @@ def set_field(a: Analysis, path: Path_, value) -> list[Edit]:
|
|
|
1208
1245
|
return [minimal_edit(a.text, apply_ops(a, [op]))]
|
|
1209
1246
|
if key == "canopen" and node_kind(path[:-1]) == "frame":
|
|
1210
1247
|
return _set_frame_type(a, path, value)
|
|
1248
|
+
if key == "sender" and node_kind(path[:-1]) == "frame":
|
|
1249
|
+
return _set_sender(a, path, value)
|
|
1211
1250
|
if not isinstance(raw_at(a, path[:-1]), dict):
|
|
1212
1251
|
# A field of a nested mapping that does not exist yet (value.name of an SDO): the mapping is added with it
|
|
1213
1252
|
parent = path[:-1]
|
|
@@ -1270,6 +1309,18 @@ def _set_frame_type(a: Analysis, path: Path_, value) -> list[Edit]:
|
|
|
1270
1309
|
return edits
|
|
1271
1310
|
|
|
1272
1311
|
|
|
1312
|
+
def _set_sender(a: Analysis, path: Path_, value) -> list[Edit]:
|
|
1313
|
+
"""The sender of a frame; a device named as the sender does not receive the frame, so it leaves the receivers."""
|
|
1314
|
+
receivers = raw_at(a, path[:-1] + ("receivers",))
|
|
1315
|
+
if not value or not isinstance(receivers, list) or value not in receivers:
|
|
1316
|
+
return _set_plain(a, path, value)
|
|
1317
|
+
rest = [r for r in receivers if r != value]
|
|
1318
|
+
ops = [{"op": "set", "path": list(path), "value": value},
|
|
1319
|
+
{"op": "set", "path": list(path[:-1] + ("receivers",)), "value": rest} if rest
|
|
1320
|
+
else {"op": "remove", "path": list(path[:-1] + ("receivers",))}]
|
|
1321
|
+
return [minimal_edit(a.text, apply_ops(a, ops))]
|
|
1322
|
+
|
|
1323
|
+
|
|
1273
1324
|
def _set_plain(a: Analysis, path: Path_, value) -> list[Edit]:
|
|
1274
1325
|
"""A scalar field without special rules: a new value, or ``None`` or "" to remove the key."""
|
|
1275
1326
|
current = raw_at(a, path)
|
|
@@ -424,7 +424,11 @@ def edit_command(ls: MapcanServer, *args) -> dict:
|
|
|
424
424
|
{error} (a signal that has no room in its frame) or {}.
|
|
425
425
|
|
|
426
426
|
The edit goes to the file the node is written in (for a new node, its owner's; a new bus goes to the main
|
|
427
|
-
file): ``uri`` is there when that is another file of the map than the document.
|
|
427
|
+
file): ``uri`` is there when that is another file of the map than the document. An included file that a
|
|
428
|
+
delete leaves adding nothing to the map (a device's own file, #136) goes instead: {delete: its uri, edits,
|
|
429
|
+
others}, the edits removing it from ``include`` (as mapcan.lsp.ops)."""
|
|
430
|
+
from ..web.devicefile import exclude_ops, part_is_empty
|
|
431
|
+
|
|
428
432
|
params = _arg(args)
|
|
429
433
|
uri = params.get("uri", "")
|
|
430
434
|
a = ls.analysis(uri) if uri else None
|
|
@@ -449,6 +453,17 @@ def edit_command(ls: MapcanServer, *args) -> dict:
|
|
|
449
453
|
change = None
|
|
450
454
|
if change is None:
|
|
451
455
|
return {}
|
|
456
|
+
if action == "delete" and view.own_file and part_is_empty(change.text):
|
|
457
|
+
from ..web.yamlops import OpError
|
|
458
|
+
|
|
459
|
+
raw = a.result.raw if isinstance(a.result.raw, dict) else {}
|
|
460
|
+
main = Path(a.result.source.file or "")
|
|
461
|
+
try:
|
|
462
|
+
out = ls.routed(a, exclude_ops(raw.get("include"), main, Path(view.own_file)), "edits")
|
|
463
|
+
except OpError:
|
|
464
|
+
out = {}
|
|
465
|
+
if out.get("edits") or out.get("others"):
|
|
466
|
+
return {**out, "delete": _file_uri(view.own_file)}
|
|
452
467
|
e = E.minimal_edit(view.text, change.text)
|
|
453
468
|
out = {"edit": {"range": [*e.start, *e.end], "text": e.text}, "cursor": list(change.cursor)}
|
|
454
469
|
if view is not a:
|
|
@@ -456,6 +471,33 @@ def edit_command(ls: MapcanServer, *args) -> dict:
|
|
|
456
471
|
return out
|
|
457
472
|
|
|
458
473
|
|
|
474
|
+
@server.command("mapcan.lsp.deviceFile")
|
|
475
|
+
def device_file_command(ls: MapcanServer, *args) -> dict:
|
|
476
|
+
"""A new device of a bus in a file of its own (#136). Argument: {uri, path: path of the bus, file}.
|
|
477
|
+
Without ``file``: {file}, the default name of the file relative to the map. With it: {create: {uri, text},
|
|
478
|
+
cursor: the device's name in the new file, edits and others: the line of ``include`` (as mapcan.lsp.ops)},
|
|
479
|
+
or {error}."""
|
|
480
|
+
from ..web.yamlops import OpError
|
|
481
|
+
|
|
482
|
+
params = _arg(args)
|
|
483
|
+
a = ls.analysis(params.get("uri", ""))
|
|
484
|
+
if a is None:
|
|
485
|
+
return {"error": _("The document is not a mapcan map.")}
|
|
486
|
+
file = params.get("file")
|
|
487
|
+
try:
|
|
488
|
+
made = E.device_file(a, tuple(params.get("path") or ()), None if file is None else str(file))
|
|
489
|
+
if file is None:
|
|
490
|
+
return made
|
|
491
|
+
out = ls.routed(a, made["ops"], "edits")
|
|
492
|
+
except (ValueError, OpError) as e:
|
|
493
|
+
return {"error": str(e)}
|
|
494
|
+
lines = made["text"].splitlines()
|
|
495
|
+
# The device is the last named item: the bus comes before it
|
|
496
|
+
line = max(i for i, ln in enumerate(lines) if ln.lstrip().startswith("- name:"))
|
|
497
|
+
return {**out, "create": {"uri": made["target"].as_uri(), "text": made["text"]},
|
|
498
|
+
"cursor": [line, lines[line].index("name:") + len("name: ")]}
|
|
499
|
+
|
|
500
|
+
|
|
459
501
|
def _params(params) -> tuple[str, tuple]:
|
|
460
502
|
"""(uri, path) from the request parameters {textDocument: {uri}, path}."""
|
|
461
503
|
get = (lambda o, k: o.get(k)) if isinstance(params, dict) else (lambda o, k: getattr(o, k, None))
|