lecore 0.2.5__tar.gz → 0.2.7__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.
Files changed (87) hide show
  1. {lecore-0.2.5/src/lecore.egg-info → lecore-0.2.7}/PKG-INFO +2 -1
  2. {lecore-0.2.5 → lecore-0.2.7}/README.md +56 -55
  3. {lecore-0.2.5 → lecore-0.2.7}/pyproject.toml +1 -1
  4. lecore-0.2.7/src/lecore/FwBuild.py +247 -0
  5. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/__main__.py +89 -75
  6. {lecore-0.2.5 → lecore-0.2.7/src/lecore.egg-info}/PKG-INFO +2 -1
  7. {lecore-0.2.5 → lecore-0.2.7}/src/lecore.egg-info/SOURCES.txt +1 -0
  8. {lecore-0.2.5 → lecore-0.2.7}/LICENSE +0 -0
  9. {lecore-0.2.5 → lecore-0.2.7}/MANIFEST.in +0 -0
  10. {lecore-0.2.5 → lecore-0.2.7}/setup.cfg +0 -0
  11. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/DeviceTests/Base.py +0 -0
  12. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/DeviceTests/LeBinTests.py +0 -0
  13. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/DeviceTests/ModbusTests.py +0 -0
  14. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/DeviceTests/Plugin.py +0 -0
  15. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/DeviceTests/__init__.py +0 -0
  16. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/DeviceBase.py +0 -0
  17. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/LeBinDev.py +0 -0
  18. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/ModbusDev.py +0 -0
  19. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/PhaseDet/ComSettings.json +0 -0
  20. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/PhaseDet/PhaseDet.py +0 -0
  21. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/PhaseDet/PhaseDetRegs.py +0 -0
  22. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/PhaseDet/PhaseDet_Modbus.json +0 -0
  23. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/PhaseDet/UpgradeSettings.json +0 -0
  24. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/PhaseDet/VisualSettings.json +0 -0
  25. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/PhaseDet/__init__.py +0 -0
  26. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/Rftb/ComSettings.json +0 -0
  27. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/Rftb/Rftb.py +0 -0
  28. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/Rftb/RftbLeBin.py +0 -0
  29. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/Rftb/UpgradeSettings.json +0 -0
  30. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/Rftb/VisualSettings.json +0 -0
  31. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/Rftb/__init__.py +0 -0
  32. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/Rftb/rftbRegs.py +0 -0
  33. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/Rftb/rftb_Modbus.json +0 -0
  34. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/Rftb/rftb_registers.json +0 -0
  35. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/RtdEmul/ComSettings.json +0 -0
  36. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/RtdEmul/RtdEmul.py +0 -0
  37. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/RtdEmul/RtdEmulRegs.py +0 -0
  38. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/RtdEmul/RtdEmul_Modbus.json +0 -0
  39. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/RtdEmul/UpgradeSettings.json +0 -0
  40. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/RtdEmul/VisualSettings.json +0 -0
  41. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/RtdEmul/__init__.py +0 -0
  42. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/Validation.py +0 -0
  43. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/VmsRtd/ComSettings.json +0 -0
  44. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/VmsRtd/UpgradeSettings.json +0 -0
  45. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/VmsRtd/VMS_RTDRegs.py +0 -0
  46. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/VmsRtd/VMS_RTD_Modbus.json +0 -0
  47. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/VmsRtd/VisualSettings.json +0 -0
  48. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/VmsRtd/VmsRtd.py +0 -0
  49. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/VmsRtd/__init__.py +0 -0
  50. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Devices/__init__.py +0 -0
  51. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Doc/ModbusTable.py +0 -0
  52. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Doc/__init__.py +0 -0
  53. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Fg320/Fg320.py +0 -0
  54. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Fg320/__init__.py +0 -0
  55. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/LeBin/RegisterMap.py +0 -0
  56. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/LeBin/SerialCom.py +0 -0
  57. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/LeBin/UpgradeFirmware.py +0 -0
  58. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/LeBin/VisualLeBin.py +0 -0
  59. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/LeBin/__init__.py +0 -0
  60. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Looger.py +0 -0
  61. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/QUICKSTART.md +0 -0
  62. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/Scaffold.py +0 -0
  63. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/TestFrame/CoreTest.py +0 -0
  64. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/TestFrame/ModbusUnitTest.py +0 -0
  65. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/TestFrame/RtuClient.py +0 -0
  66. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/TestFrame/Runner.py +0 -0
  67. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/TestFrame/TestUtils.py +0 -0
  68. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/TestFrame/__init__.py +0 -0
  69. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/AppLogging.py +0 -0
  70. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/Crc32.py +0 -0
  71. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/HelpAbout.py +0 -0
  72. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/MbClient.py +0 -0
  73. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/MbUpgrade.py +0 -0
  74. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/ReadWrite.py +0 -0
  75. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/RegMap.py +0 -0
  76. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/TextMbApp.py +0 -0
  77. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/TextMbAppCss.tcss +0 -0
  78. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/VisualMbApp.py +0 -0
  79. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/VmSettings.py +0 -0
  80. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/VisualModbus/__init__.py +0 -0
  81. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/__init__.py +0 -0
  82. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/_version.py +0 -0
  83. {lecore-0.2.5 → lecore-0.2.7}/src/lecore/py.typed +0 -0
  84. {lecore-0.2.5 → lecore-0.2.7}/src/lecore.egg-info/dependency_links.txt +0 -0
  85. {lecore-0.2.5 → lecore-0.2.7}/src/lecore.egg-info/entry_points.txt +0 -0
  86. {lecore-0.2.5 → lecore-0.2.7}/src/lecore.egg-info/requires.txt +0 -0
  87. {lecore-0.2.5 → lecore-0.2.7}/src/lecore.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lecore
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Logic Elements core utilities
5
5
  Author-email: Jan Bartovský <jan.bartovsky@logicelements.cz>
6
6
  License-Expression: MIT
@@ -81,6 +81,7 @@ New device class (in the directory with the register map): `python -m lecore new
81
81
  - [Device API](doc/api.md) - Modbus and LeBin device: read, write, firmware upgrade
82
82
  - [User interfaces](doc/user-interfaces.md) - graphical and terminal register browser
83
83
  - [Devices](doc/devices.md) - RTD Emulator, VMS-1502 RTD Meter, Phase Detector, FG-320
84
+ - [Firmware post-build](doc/fwbuild.md) - FW information block and CRC (replaces BinBuild.exe)
84
85
  - [Build and release](doc/build.md)
85
86
  - [Looger](doc/looger.md)
86
87
 
@@ -1,55 +1,56 @@
1
- # LE core package
2
-
3
- [![CI](https://github.com/LogicElements/lecore/actions/workflows/ci.yml/badge.svg)](https://github.com/LogicElements/lecore/actions/workflows/ci.yml)
4
- [![PyPI](https://img.shields.io/pypi/v/lecore)](https://pypi.org/project/lecore/)
5
- [![Python](https://img.shields.io/pypi/pyversions/lecore)](https://pypi.org/project/lecore/)
6
- [![License](https://img.shields.io/pypi/l/lecore)](LICENSE)
7
-
8
- Core Python utilities for Logic Elements:
9
- - Looger client
10
- - Proprietary LeBin protocol
11
- - Proprietary Modbus RTU register map handling - VisualModbus
12
- - Test frame
13
- - Logic Elements devices
14
-
15
- ## Install
16
-
17
- Requires Python 3.9 or newer.
18
-
19
- ```
20
- pip install lecore
21
- pip install --upgrade lecore
22
- ```
23
-
24
- Optional extras: `lecore[extra]` (graphical UI, Word documentation) and `lecore[tui]` (terminal UI).
25
-
26
- ## Usage
27
-
28
- ```python
29
- import lecore.Devices.RtdEmul as RE
30
-
31
- # Create RTD emulator object and open communication at COM2 (default is COM1)
32
- rtd = RE.RtdEmul()
33
- rtd.open(comport='COM2')
34
-
35
- # Read serial number
36
- sn = rtd.read(RE.Reg.FACT_SERIAL_NUMBER)
37
-
38
- # Write emulated temperature at output 1
39
- rtd.write(RE.Reg.EMUL_TEMPERATURE_1, 30.5)
40
- ```
41
-
42
- New device class (in the directory with the register map): `python -m lecore new-device MyDevice`, quick guide: `python -m lecore guide`.
43
-
44
- ## Documentation
45
-
46
- - [Creating a new device](doc/new-device.md)
47
- - [Device API](doc/api.md) - Modbus and LeBin device: read, write, firmware upgrade
48
- - [User interfaces](doc/user-interfaces.md) - graphical and terminal register browser
49
- - [Devices](doc/devices.md) - RTD Emulator, VMS-1502 RTD Meter, Phase Detector, FG-320
50
- - [Build and release](doc/build.md)
51
- - [Looger](doc/looger.md)
52
-
53
- ## License
54
-
55
- MIT, see [LICENSE](LICENSE).
1
+ # LE core package
2
+
3
+ [![CI](https://github.com/LogicElements/lecore/actions/workflows/ci.yml/badge.svg)](https://github.com/LogicElements/lecore/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/lecore)](https://pypi.org/project/lecore/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/lecore)](https://pypi.org/project/lecore/)
6
+ [![License](https://img.shields.io/pypi/l/lecore)](LICENSE)
7
+
8
+ Core Python utilities for Logic Elements:
9
+ - Looger client
10
+ - Proprietary LeBin protocol
11
+ - Proprietary Modbus RTU register map handling - VisualModbus
12
+ - Test frame
13
+ - Logic Elements devices
14
+
15
+ ## Install
16
+
17
+ Requires Python 3.9 or newer.
18
+
19
+ ```
20
+ pip install lecore
21
+ pip install --upgrade lecore
22
+ ```
23
+
24
+ Optional extras: `lecore[extra]` (graphical UI, Word documentation) and `lecore[tui]` (terminal UI).
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ import lecore.Devices.RtdEmul as RE
30
+
31
+ # Create RTD emulator object and open communication at COM2 (default is COM1)
32
+ rtd = RE.RtdEmul()
33
+ rtd.open(comport='COM2')
34
+
35
+ # Read serial number
36
+ sn = rtd.read(RE.Reg.FACT_SERIAL_NUMBER)
37
+
38
+ # Write emulated temperature at output 1
39
+ rtd.write(RE.Reg.EMUL_TEMPERATURE_1, 30.5)
40
+ ```
41
+
42
+ New device class (in the directory with the register map): `python -m lecore new-device MyDevice`, quick guide: `python -m lecore guide`.
43
+
44
+ ## Documentation
45
+
46
+ - [Creating a new device](doc/new-device.md)
47
+ - [Device API](doc/api.md) - Modbus and LeBin device: read, write, firmware upgrade
48
+ - [User interfaces](doc/user-interfaces.md) - graphical and terminal register browser
49
+ - [Devices](doc/devices.md) - RTD Emulator, VMS-1502 RTD Meter, Phase Detector, FG-320
50
+ - [Firmware post-build](doc/fwbuild.md) - FW information block and CRC (replaces BinBuild.exe)
51
+ - [Build and release](doc/build.md)
52
+ - [Looger](doc/looger.md)
53
+
54
+ ## License
55
+
56
+ MIT, see [LICENSE](LICENSE).
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "lecore"
7
7
  description = "Logic Elements core utilities"
8
- version = "0.2.5"
8
+ version = "0.2.7"
9
9
  authors = [
10
10
  { name="Jan Bartovský", email="jan.bartovsky@logicelements.cz" },
11
11
  ]
@@ -0,0 +1,247 @@
1
+ """
2
+ Firmware post-build: stamps the FW information block (version, build date, CRC, size) into a binary.
3
+
4
+ Python port of the legacy ``BinBuild.exe``, usable on any platform: ``python -m lecore binbuild``.
5
+ The CRC is the one of the STM32 hardware CRC unit: polynom 0x04C11DB7, init 0xFFFFFFFF, 32-bit little-endian
6
+ words, no reflection and no final XOR. The same CRC is produced for all targets.
7
+
8
+ Targets 1-7 are verified byte by byte against BinBuild.exe (build of 2024-09). Target 8 is not implemented.
9
+ Intel HEX files are supported too (except target 1): the image from the lowest address is processed like a binary,
10
+ gaps are filled with 0x00 (like ``objcopy -O binary``) and the result is written as a contiguous HEX.
11
+ """
12
+ import datetime
13
+ import struct
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ _POLYNOM = 0x04C11DB7
19
+ _MASK = 0xFFFFFFFF
20
+
21
+
22
+ def _make_table():
23
+ table = []
24
+ for i in range(256):
25
+ value = i << 24
26
+ for _ in range(8):
27
+ value = ((value << 1) ^ _POLYNOM if value & 0x80000000 else value << 1) & _MASK
28
+ table.append(value)
29
+ return table
30
+
31
+
32
+ _TABLE = _make_table()
33
+
34
+
35
+ def crc32(data: bytes, crc: int = _MASK) -> int:
36
+ """
37
+ CRC of the STM32 hardware CRC unit over 32-bit little-endian words
38
+ :param data: Data, its length must be a multiple of 4
39
+ :param crc: Initial value
40
+ :return: CRC
41
+ """
42
+ if len(data) % 4:
43
+ raise ValueError("data length must be a multiple of 4")
44
+ table = _TABLE
45
+ for word in struct.unpack(f"<{len(data) // 4}I", data):
46
+ crc ^= word
47
+ crc = ((crc << 8) & _MASK) ^ table[crc >> 24]
48
+ crc = ((crc << 8) & _MASK) ^ table[crc >> 24]
49
+ crc = ((crc << 8) & _MASK) ^ table[crc >> 24]
50
+ crc = ((crc << 8) & _MASK) ^ table[crc >> 24]
51
+ return crc
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class Target:
56
+ """
57
+ Layout of the FW information block
58
+ :param offset: Offset of the block in the binary, None = prepended to the file (LeCloud)
59
+ :param decimal: Version and build date are stored as decimal numbers (otherwise BCD hex)
60
+ :param clear_from: Bytes of the 32 B block from this index are taken as 0xFF in the CRC (None = nothing)
61
+ :param description: Description for the command line help
62
+ """
63
+ offset: Optional[int]
64
+ decimal: bool
65
+ clear_from: Optional[int]
66
+ description: str
67
+
68
+
69
+ TARGETS = {
70
+ 1: Target(None, False, None, "beginning of the file for LeCloud server (JLT products)"),
71
+ 2: Target(512, False, None, "512 B offset in the FW (FG320)"),
72
+ 3: Target(512, False, None, "512 B offset in the FW (VMS)"),
73
+ 4: Target(192, False, None, "192 B offset in the FW (small alarm), device ID in the block"),
74
+ 5: Target(1024, True, None, "1024 B offset in the FW, decimal information (SBD)"),
75
+ 6: Target(512, True, 16, "512 B offset in the FW, decimal information (SIR1)"),
76
+ 7: Target(512, True, 16, "512 B offset in the FW, decimal information (Fixture)"),
77
+ }
78
+
79
+ _HEX_MAX_SIZE = 16 * 1024 * 1024 # protection against huge gaps (e.g. option bytes far away from the FW)
80
+ _HEX_RECORD = 16
81
+
82
+ _LEGACY_OFFSET = 512 # target 1 checks the same free space as the targets with a 512 B offset
83
+
84
+
85
+ def build(data: bytes, target: int, version: str, dev_id: str = "", date: Optional[datetime.date] = None) -> tuple:
86
+ """
87
+ Stamp the FW information block into a binary
88
+ :param data: Binary content (padded with zeros to a multiple of 4)
89
+ :param target: Target number, see ``TARGETS``
90
+ :param version: FW version, hexadecimal (BCD) like in BinBuild
91
+ :param dev_id: Device ID (max 4 characters), used by target 4 only
92
+ :param date: Build date (default today)
93
+ :return: Tuple (new binary content, CRC)
94
+ """
95
+ if target not in TARGETS:
96
+ raise ValueError(f"unknown target {target}, valid: {', '.join(map(str, TARGETS))}")
97
+ layout = TARGETS[target]
98
+ version_hex = int(version, 16)
99
+ try:
100
+ version_dec = int(version, 10)
101
+ except ValueError:
102
+ version_dec = 0 # BinBuild stores 0 when the version is not a decimal number
103
+ assembly = (date or datetime.date.today()).strftime("%Y%m%d")
104
+ if target == 4 and (len(dev_id) > 4 or not dev_id.isascii()):
105
+ raise ValueError(f"device ID '{dev_id}' must be at most 4 ASCII characters")
106
+
107
+ binary = bytearray(data)
108
+ binary += bytes(-len(binary) % 4)
109
+ size = len(binary)
110
+
111
+ offset = layout.offset if layout.offset is not None else _LEGACY_OFFSET
112
+ if target == 4:
113
+ free = [(offset, offset + 32, 0x00)]
114
+ else:
115
+ length = 16 if target >= 6 else 32
116
+ free = [(offset - 32, offset, 0x00), (offset, offset + length, 0xFF)]
117
+ for start, end, value in free:
118
+ if end > size or any(b != value for b in binary[start:end]):
119
+ raise ValueError("there must be free space for the FW information block "
120
+ f"(bytes {start}..{end - 1} must be 0x{value:02X})")
121
+
122
+ crc_data = bytearray(binary)
123
+ if layout.clear_from is not None:
124
+ crc_data[offset + layout.clear_from:offset + 32] = bytes([0xFF]) * (32 - layout.clear_from)
125
+ crc = crc32(bytes(crc_data))
126
+
127
+ if target == 4:
128
+ info = struct.pack("<I4sIII", version_hex, dev_id.encode("ascii"), size, crc, int(assembly, 16))
129
+ elif layout.decimal:
130
+ info = struct.pack("<IIII", version_dec, int(assembly), crc, size)
131
+ else:
132
+ info = struct.pack("<IIII", version_hex, int(assembly, 16), crc, size)
133
+
134
+ if layout.offset is None:
135
+ return info + bytes(binary), crc
136
+ stamped = bytes(binary)
137
+ return stamped[:offset] + info + stamped[offset + len(info):], crc
138
+
139
+
140
+ def read_hex(text: str) -> tuple:
141
+ """
142
+ Parse an Intel HEX file into a contiguous image, gaps are filled with 0x00
143
+ :param text: HEX file content
144
+ :return: Tuple (base address, image, start address record line or None)
145
+ """
146
+ chunks = {}
147
+ upper = 0
148
+ start = None
149
+ for number, line in enumerate(text.splitlines(), 1):
150
+ line = line.strip()
151
+ if not line:
152
+ continue
153
+ try:
154
+ if line[0] != ":":
155
+ raise ValueError
156
+ record = bytes.fromhex(line[1:])
157
+ except ValueError:
158
+ raise ValueError(f"line {number}: not an Intel HEX record") from None
159
+ if len(record) < 5 or len(record) != record[0] + 5:
160
+ raise ValueError(f"line {number}: wrong record length")
161
+ if sum(record) & 0xFF:
162
+ raise ValueError(f"line {number}: wrong checksum")
163
+ rtype, payload = record[3], record[4:-1]
164
+ if rtype == 0x00:
165
+ address = upper + int.from_bytes(record[1:3], "big")
166
+ chunks[address] = chunks.get(address, b"") + payload
167
+ elif rtype == 0x01:
168
+ break
169
+ elif rtype == 0x02:
170
+ upper = int.from_bytes(payload, "big") << 4
171
+ elif rtype == 0x04:
172
+ upper = int.from_bytes(payload, "big") << 16
173
+ elif rtype in (0x03, 0x05):
174
+ start = line
175
+ else:
176
+ raise ValueError(f"line {number}: unknown record type {rtype:02X}")
177
+ if not chunks:
178
+ raise ValueError("no data in the HEX file")
179
+ base = min(chunks)
180
+ size = max(address + len(payload) for address, payload in chunks.items()) - base
181
+ if size > _HEX_MAX_SIZE:
182
+ raise ValueError(f"HEX image is too large ({size} B from 0x{base:08X}), check for data at distant addresses")
183
+ image = bytearray(size)
184
+ for address, payload in chunks.items():
185
+ image[address - base:address - base + len(payload)] = payload
186
+ return base, bytes(image), start
187
+
188
+
189
+ def _hex_record(rtype: int, address: int, payload: bytes) -> str:
190
+ record = bytes([len(payload)]) + address.to_bytes(2, "big") + bytes([rtype]) + payload
191
+ return ":" + (record + bytes([-sum(record) & 0xFF])).hex().upper()
192
+
193
+
194
+ def write_hex(base: int, image: bytes, start: Optional[str] = None) -> str:
195
+ """
196
+ Write a contiguous image as an Intel HEX file
197
+ :param base: Address of the image
198
+ :param image: Image content
199
+ :param start: Start address record line (from ``read_hex``)
200
+ :return: HEX file content
201
+ """
202
+ lines = []
203
+ upper = None
204
+ address = base
205
+ end = base + len(image)
206
+ while address < end:
207
+ if address >> 16 != upper:
208
+ upper = address >> 16
209
+ lines.append(_hex_record(0x04, 0, upper.to_bytes(2, "big")))
210
+ length = min(_HEX_RECORD, end - address, 0x10000 - (address & 0xFFFF))
211
+ lines.append(_hex_record(0x00, address & 0xFFFF, image[address - base:address - base + length]))
212
+ address += length
213
+ if start:
214
+ lines.append(start)
215
+ lines.append(_hex_record(0x01, 0, b""))
216
+ return "\n".join(lines) + "\n"
217
+
218
+
219
+ def build_file(target: int, path, version: str, suffix: str = "", out_dir=None,
220
+ date: Optional[datetime.date] = None) -> tuple:
221
+ """
222
+ Stamp the FW information block into a binary or Intel HEX (``.hex``) file, the result is written next to it
223
+ (or to ``out_dir``) as ``<name>[-<suffix>]-<yyyymmdd>-<version>.bin`` (``.hex``)
224
+ :param target: Target number, see ``TARGETS``
225
+ :param path: Binary or Intel HEX file
226
+ :param version: FW version, hexadecimal (BCD)
227
+ :param suffix: Suffix of the output name, also used as the device ID of target 4
228
+ :param out_dir: Output directory (default directory of the input file)
229
+ :param date: Build date (default today)
230
+ :return: Tuple (output path, CRC)
231
+ """
232
+ path = Path(path)
233
+ date = date or datetime.date.today()
234
+ is_hex = path.suffix.lower() == ".hex"
235
+ if is_hex:
236
+ if TARGETS.get(target) is not None and TARGETS[target].offset is None:
237
+ raise ValueError(f"target {target} prepends the block to the file, it is not supported for HEX files")
238
+ base, image, start = read_hex(path.read_text(encoding="ascii"))
239
+ result, crc = build(image, target, version, suffix, date)
240
+ content = write_hex(base, result, start).encode("ascii")
241
+ else:
242
+ content, crc = build(path.read_bytes(), target, version, suffix, date)
243
+ name = f"{path.stem}{'-' + suffix if suffix else ''}-{date:%Y%m%d}-{int(version, 16):X}"
244
+ name += ".hex" if is_hex else ".bin"
245
+ out = Path(out_dir) / name if out_dir else path.with_name(name)
246
+ out.write_bytes(content)
247
+ return out, crc
@@ -1,75 +1,89 @@
1
- """
2
- Command line jump-start for lecore, see ``python -m lecore --help``
3
- """
4
- import argparse
5
- import sys
6
- import warnings
7
-
8
- from .Scaffold import guide, new_device, generate_regs, generate_tests
9
-
10
-
11
- def _show_warning(message, category, filename, lineno, file=None, line=None):
12
- print(f"warning: {message}", file=sys.stderr)
13
-
14
-
15
- def main(argv=None) -> int:
16
- warnings.showwarning = _show_warning
17
- parser = argparse.ArgumentParser(prog="lecore", description="Logic Elements core utilities")
18
- sub = parser.add_subparsers(dest="cmd", required=True)
19
-
20
- sub.add_parser("guide", help="print quick guide for creating devices with lecore")
21
-
22
- p = sub.add_parser("new-device", help="create device class and missing configuration files next to the "
23
- "register map")
24
- p.add_argument("name", help="device class name, e.g., MyDevice")
25
- proto = p.add_mutually_exclusive_group()
26
- proto.add_argument("--modbus", dest="protocol", action="store_const", const="modbus",
27
- help="Modbus device (default: detected from register map)")
28
- proto.add_argument("--lebin", dest="protocol", action="store_const", const="lebin", help="LeBin device")
29
- p.add_argument("--dir", default=".", help="directory with register map (default: current)")
30
- p.add_argument("--reg-map", help="register map JSON in the directory (default: the single one found)")
31
- p.add_argument("--template", action="store_true",
32
- help="create minimal register map template and *Regs.py when no register map is present")
33
- p.add_argument("--gen-regs", action="store_true", help="generate missing *Regs.py from the register map")
34
- p.add_argument("--slave", type=int, default=1, help="default Modbus slave address")
35
- p.add_argument("--comport", default="COM1", help="default COM port")
36
- p.add_argument("--baud-rate", type=int, default=19200, help="default baud rate")
37
- p.add_argument("--parity", default="E", choices=("N", "E", "O"), help="default parity")
38
- p.add_argument("--usb", nargs=3, metavar=("VID", "PID", "SN"), help="LeBin USB identification for search_com()")
39
- p.add_argument("--force", action="store_true", help="overwrite existing device module and configuration files")
40
-
41
- p = sub.add_parser("new-tests", help="create missing communication test files (test_modbus.py, test_lebin.py) "
42
- "for device classes in a directory")
43
- p.add_argument("--dir", default=".", help="directory with device modules (default: current)")
44
- p.add_argument("--force", action="store_true", help="overwrite existing test files")
45
-
46
- p = sub.add_parser("regs", help="generate class with register name constants from a register map")
47
- p.add_argument("reg_map", help="register map JSON (Modbus or LeBin)")
48
- p.add_argument("--class", dest="class_name", default="Regs", help="generated class name")
49
- p.add_argument("-o", "--output", help="output Python file (default: print)")
50
-
51
- args = parser.parse_args(argv)
52
- try:
53
- if args.cmd == "guide":
54
- print(guide())
55
- elif args.cmd == "new-device":
56
- usb = (int(args.usb[0], 0), int(args.usb[1], 0), args.usb[2]) if args.usb else None
57
- result = new_device(args.name, args.protocol, args.dir, args.reg_map, args.template, args.gen_regs,
58
- args.slave, args.comport, args.baud_rate, args.parity, usb, args.force)
59
- for path, status in result.items():
60
- print(f"{status:12}{path}")
61
- elif args.cmd == "new-tests":
62
- for path, status in generate_tests(args.dir, args.force).items():
63
- print(f"{status:12}{path}")
64
- elif args.cmd == "regs":
65
- src = generate_regs(args.reg_map, args.class_name, args.output)
66
- if args.output is None:
67
- print(src, end="")
68
- except (ValueError, OSError) as e:
69
- print(f"error: {e}", file=sys.stderr)
70
- return 1
71
- return 0
72
-
73
-
74
- if __name__ == "__main__":
75
- sys.exit(main())
1
+ """
2
+ Command line jump-start for lecore, see ``python -m lecore --help``
3
+ """
4
+ import argparse
5
+ import sys
6
+ import warnings
7
+
8
+ from .Scaffold import guide, new_device, generate_regs, generate_tests
9
+ from .FwBuild import TARGETS, build_file
10
+
11
+
12
+ def _show_warning(message, category, filename, lineno, file=None, line=None):
13
+ print(f"warning: {message}", file=sys.stderr)
14
+
15
+
16
+ def main(argv=None) -> int:
17
+ warnings.showwarning = _show_warning
18
+ parser = argparse.ArgumentParser(prog="lecore", description="Logic Elements core utilities")
19
+ sub = parser.add_subparsers(dest="cmd", required=True)
20
+
21
+ sub.add_parser("guide", help="print quick guide for creating devices with lecore")
22
+
23
+ p = sub.add_parser("new-device", help="create device class and missing configuration files next to the "
24
+ "register map")
25
+ p.add_argument("name", help="device class name, e.g., MyDevice")
26
+ proto = p.add_mutually_exclusive_group()
27
+ proto.add_argument("--modbus", dest="protocol", action="store_const", const="modbus",
28
+ help="Modbus device (default: detected from register map)")
29
+ proto.add_argument("--lebin", dest="protocol", action="store_const", const="lebin", help="LeBin device")
30
+ p.add_argument("--dir", default=".", help="directory with register map (default: current)")
31
+ p.add_argument("--reg-map", help="register map JSON in the directory (default: the single one found)")
32
+ p.add_argument("--template", action="store_true",
33
+ help="create minimal register map template and *Regs.py when no register map is present")
34
+ p.add_argument("--gen-regs", action="store_true", help="generate missing *Regs.py from the register map")
35
+ p.add_argument("--slave", type=int, default=1, help="default Modbus slave address")
36
+ p.add_argument("--comport", default="COM1", help="default COM port")
37
+ p.add_argument("--baud-rate", type=int, default=19200, help="default baud rate")
38
+ p.add_argument("--parity", default="E", choices=("N", "E", "O"), help="default parity")
39
+ p.add_argument("--usb", nargs=3, metavar=("VID", "PID", "SN"), help="LeBin USB identification for search_com()")
40
+ p.add_argument("--force", action="store_true", help="overwrite existing device module and configuration files")
41
+
42
+ p = sub.add_parser("new-tests", help="create missing communication test files (test_modbus.py, test_lebin.py) "
43
+ "for device classes in a directory")
44
+ p.add_argument("--dir", default=".", help="directory with device modules (default: current)")
45
+ p.add_argument("--force", action="store_true", help="overwrite existing test files")
46
+
47
+ p = sub.add_parser("regs", help="generate class with register name constants from a register map")
48
+ p.add_argument("reg_map", help="register map JSON (Modbus or LeBin)")
49
+ p.add_argument("--class", dest="class_name", default="Regs", help="generated class name")
50
+ p.add_argument("-o", "--output", help="output Python file (default: print)")
51
+
52
+ p = sub.add_parser("binbuild", help="stamp the FW information block (CRC, version, size) into a binary or HEX "
53
+ "(replaces BinBuild.exe)",
54
+ formatter_class=argparse.RawDescriptionHelpFormatter,
55
+ epilog="targets:\n" + "\n".join(f" {n} - {t.description}" for n, t in TARGETS.items()))
56
+ p.add_argument("target", type=int, choices=sorted(TARGETS), help="FW information block layout")
57
+ p.add_argument("file", help="binary or Intel HEX (.hex) file")
58
+ p.add_argument("version", help="FW version, hexadecimal (BCD)")
59
+ p.add_argument("--suffix", default="", help="suffix of the output name, device ID for target 4")
60
+ p.add_argument("--out-dir", help="output directory (default: directory of the binary)")
61
+
62
+ args = parser.parse_args(argv)
63
+ try:
64
+ if args.cmd == "guide":
65
+ print(guide())
66
+ elif args.cmd == "new-device":
67
+ usb = (int(args.usb[0], 0), int(args.usb[1], 0), args.usb[2]) if args.usb else None
68
+ result = new_device(args.name, args.protocol, args.dir, args.reg_map, args.template, args.gen_regs,
69
+ args.slave, args.comport, args.baud_rate, args.parity, usb, args.force)
70
+ for path, status in result.items():
71
+ print(f"{status:12}{path}")
72
+ elif args.cmd == "new-tests":
73
+ for path, status in generate_tests(args.dir, args.force).items():
74
+ print(f"{status:12}{path}")
75
+ elif args.cmd == "regs":
76
+ src = generate_regs(args.reg_map, args.class_name, args.output)
77
+ if args.output is None:
78
+ print(src, end="")
79
+ elif args.cmd == "binbuild":
80
+ out, crc = build_file(args.target, args.file, args.version, args.suffix, args.out_dir)
81
+ print(f"{out}\nNew File created with CRC value {crc:08X}")
82
+ except (ValueError, OSError) as e:
83
+ print(f"error: {e}", file=sys.stderr)
84
+ return 1
85
+ return 0
86
+
87
+
88
+ if __name__ == "__main__":
89
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lecore
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Logic Elements core utilities
5
5
  Author-email: Jan Bartovský <jan.bartovsky@logicelements.cz>
6
6
  License-Expression: MIT
@@ -81,6 +81,7 @@ New device class (in the directory with the register map): `python -m lecore new
81
81
  - [Device API](doc/api.md) - Modbus and LeBin device: read, write, firmware upgrade
82
82
  - [User interfaces](doc/user-interfaces.md) - graphical and terminal register browser
83
83
  - [Devices](doc/devices.md) - RTD Emulator, VMS-1502 RTD Meter, Phase Detector, FG-320
84
+ - [Firmware post-build](doc/fwbuild.md) - FW information block and CRC (replaces BinBuild.exe)
84
85
  - [Build and release](doc/build.md)
85
86
  - [Looger](doc/looger.md)
86
87
 
@@ -2,6 +2,7 @@ LICENSE
2
2
  MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
+ src/lecore/FwBuild.py
5
6
  src/lecore/Looger.py
6
7
  src/lecore/QUICKSTART.md
7
8
  src/lecore/Scaffold.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes