lecore 0.2.0__tar.gz → 0.2.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.
Files changed (80) hide show
  1. {lecore-0.2.0/src/lecore.egg-info → lecore-0.2.2}/PKG-INFO +7 -3
  2. {lecore-0.2.0 → lecore-0.2.2}/README.md +7 -3
  3. {lecore-0.2.0 → lecore-0.2.2}/pyproject.toml +5 -2
  4. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/LeBinDev.py +269 -261
  5. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/ModbusDev.py +10 -0
  6. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/LeBin/SerialCom.py +2 -1
  7. lecore-0.2.2/src/lecore/QUICKSTART.md +98 -0
  8. lecore-0.2.2/src/lecore/Scaffold.py +583 -0
  9. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/AppLogging.py +35 -11
  10. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/ReadWrite.py +1 -1
  11. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/__init__.py +8 -1
  12. lecore-0.2.2/src/lecore/__main__.py +67 -0
  13. {lecore-0.2.0 → lecore-0.2.2/src/lecore.egg-info}/PKG-INFO +7 -3
  14. {lecore-0.2.0 → lecore-0.2.2}/src/lecore.egg-info/SOURCES.txt +4 -0
  15. lecore-0.2.2/src/lecore.egg-info/entry_points.txt +2 -0
  16. {lecore-0.2.0 → lecore-0.2.2}/LICENSE +0 -0
  17. {lecore-0.2.0 → lecore-0.2.2}/MANIFEST.in +0 -0
  18. {lecore-0.2.0 → lecore-0.2.2}/setup.cfg +0 -0
  19. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/DeviceBase.py +0 -0
  20. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/PhaseDet/ComSettings.json +0 -0
  21. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/PhaseDet/PhaseDet.py +0 -0
  22. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/PhaseDet/PhaseDetRegs.py +0 -0
  23. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/PhaseDet/PhaseDet_Modbus.json +0 -0
  24. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/PhaseDet/UpgradeSettings.json +0 -0
  25. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/PhaseDet/VisualSettings.json +0 -0
  26. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/PhaseDet/__init__.py +0 -0
  27. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/Rftb/ComSettings.json +0 -0
  28. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/Rftb/Rftb.py +0 -0
  29. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/Rftb/RftbLeBin.py +0 -0
  30. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/Rftb/UpgradeSettings.json +0 -0
  31. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/Rftb/VisualSettings.json +0 -0
  32. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/Rftb/__init__.py +0 -0
  33. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/Rftb/rftbRegs.py +0 -0
  34. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/Rftb/rftb_Modbus.json +0 -0
  35. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/Rftb/rftb_registers.json +0 -0
  36. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/RtdEmul/ComSettings.json +0 -0
  37. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/RtdEmul/RtdEmul.py +0 -0
  38. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/RtdEmul/RtdEmulRegs.py +0 -0
  39. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/RtdEmul/RtdEmul_Modbus.json +0 -0
  40. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/RtdEmul/UpgradeSettings.json +0 -0
  41. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/RtdEmul/VisualSettings.json +0 -0
  42. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/RtdEmul/__init__.py +0 -0
  43. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/Validation.py +0 -0
  44. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/VmsRtd/ComSettings.json +0 -0
  45. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/VmsRtd/UpgradeSettings.json +0 -0
  46. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/VmsRtd/VMS_RTDRegs.py +0 -0
  47. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/VmsRtd/VMS_RTD_Modbus.json +0 -0
  48. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/VmsRtd/VisualSettings.json +0 -0
  49. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/VmsRtd/VmsRtd.py +0 -0
  50. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/VmsRtd/__init__.py +0 -0
  51. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Devices/__init__.py +0 -0
  52. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Doc/ModbusTable.py +0 -0
  53. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Doc/__init__.py +0 -0
  54. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Fg320/Fg320.py +0 -0
  55. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Fg320/__init__.py +0 -0
  56. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/LeBin/RegisterMap.py +0 -0
  57. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/LeBin/UpgradeFirmware.py +0 -0
  58. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/LeBin/VisualLeBin.py +0 -0
  59. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/LeBin/__init__.py +0 -0
  60. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/Looger.py +0 -0
  61. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/TestFrame/CoreTest.py +0 -0
  62. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/TestFrame/ModbusUnitTest.py +0 -0
  63. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/TestFrame/RtuClient.py +0 -0
  64. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/TestFrame/Runner.py +0 -0
  65. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/TestFrame/TestUtils.py +0 -0
  66. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/TestFrame/__init__.py +0 -0
  67. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/Crc32.py +0 -0
  68. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/HelpAbout.py +0 -0
  69. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/MbClient.py +0 -0
  70. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/MbUpgrade.py +0 -0
  71. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/RegMap.py +0 -0
  72. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/TextMbApp.py +0 -0
  73. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/TextMbAppCss.tcss +0 -0
  74. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/VisualMbApp.py +0 -0
  75. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/VmSettings.py +0 -0
  76. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/VisualModbus/__init__.py +0 -0
  77. {lecore-0.2.0 → lecore-0.2.2}/src/lecore/py.typed +0 -0
  78. {lecore-0.2.0 → lecore-0.2.2}/src/lecore.egg-info/dependency_links.txt +0 -0
  79. {lecore-0.2.0 → lecore-0.2.2}/src/lecore.egg-info/requires.txt +0 -0
  80. {lecore-0.2.0 → lecore-0.2.2}/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.0
3
+ Version: 0.2.2
4
4
  Summary: Logic Elements core utilities
5
5
  Author-email: Jan Bartovský <jan.bartovsky@logicelements.cz>
6
6
  License-Expression: MIT
@@ -34,6 +34,7 @@ Dynamic: license-file
34
34
 
35
35
  # LE core package
36
36
 
37
+ [![CI](https://github.com/LogicElements/lecore/actions/workflows/ci.yml/badge.svg)](https://github.com/LogicElements/lecore/actions/workflows/ci.yml)
37
38
  [![PyPI](https://img.shields.io/pypi/v/lecore)](https://pypi.org/project/lecore/)
38
39
  [![Python](https://img.shields.io/pypi/pyversions/lecore)](https://pypi.org/project/lecore/)
39
40
  [![License](https://img.shields.io/pypi/l/lecore)](LICENSE)
@@ -72,13 +73,16 @@ sn = rtd.read(RE.Reg.FACT_SERIAL_NUMBER)
72
73
  rtd.write(RE.Reg.EMUL_TEMPERATURE_1, 30.5)
73
74
  ```
74
75
 
76
+ New device class (in the directory with the register map): `python -m lecore new-device MyDevice`, quick guide: `python -m lecore guide`.
77
+
75
78
  ## Documentation
76
79
 
77
- - [Devices](doc/devices.md) - RTD Emulator, VMS-1502 RTD Meter, Phase Detector, FG-320
78
80
  - [Creating a new device](doc/new-device.md)
79
- - [Looger](doc/looger.md)
81
+ - [Device API](doc/api.md) - Modbus and LeBin device: read, write, firmware upgrade
80
82
  - [User interfaces](doc/user-interfaces.md) - graphical and terminal register browser
83
+ - [Devices](doc/devices.md) - RTD Emulator, VMS-1502 RTD Meter, Phase Detector, FG-320
81
84
  - [Build and release](doc/build.md)
85
+ - [Looger](doc/looger.md)
82
86
 
83
87
  ## License
84
88
 
@@ -1,5 +1,6 @@
1
1
  # LE core package
2
2
 
3
+ [![CI](https://github.com/LogicElements/lecore/actions/workflows/ci.yml/badge.svg)](https://github.com/LogicElements/lecore/actions/workflows/ci.yml)
3
4
  [![PyPI](https://img.shields.io/pypi/v/lecore)](https://pypi.org/project/lecore/)
4
5
  [![Python](https://img.shields.io/pypi/pyversions/lecore)](https://pypi.org/project/lecore/)
5
6
  [![License](https://img.shields.io/pypi/l/lecore)](LICENSE)
@@ -38,13 +39,16 @@ sn = rtd.read(RE.Reg.FACT_SERIAL_NUMBER)
38
39
  rtd.write(RE.Reg.EMUL_TEMPERATURE_1, 30.5)
39
40
  ```
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
+
41
44
  ## Documentation
42
45
 
43
- - [Devices](doc/devices.md) - RTD Emulator, VMS-1502 RTD Meter, Phase Detector, FG-320
44
- - [Creating a new device](doc/new-device.md)
45
- - [Looger](doc/looger.md)
46
+ - [Creating a new device](doc/new-device.md)
47
+ - [Device API](doc/api.md) - Modbus and LeBin device: read, write, firmware upgrade
46
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
47
50
  - [Build and release](doc/build.md)
51
+ - [Looger](doc/looger.md)
48
52
 
49
53
  ## License
50
54
 
@@ -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.0"
8
+ version = "0.2.2"
9
9
  authors = [
10
10
  { name="Jan Bartovský", email="jan.bartovsky@logicelements.cz" },
11
11
  ]
@@ -33,6 +33,9 @@ dependencies = [
33
33
  "unittest-xml-reporting",
34
34
  ]
35
35
 
36
+ [project.scripts]
37
+ lecore = "lecore.__main__:main"
38
+
36
39
  [project.optional-dependencies]
37
40
  extra = [
38
41
  "FreeSimpleGUI",
@@ -49,7 +52,7 @@ dev = [
49
52
  where = ["src"]
50
53
 
51
54
  [tool.setuptools.package-data]
52
- "lecore" = ["py.typed"]
55
+ "lecore" = ["py.typed", "QUICKSTART.md"]
53
56
  "lecore.Devices.RtdEmul" = ["*.json"]
54
57
  "lecore.Devices.VmsRtd" = ["*.json"]
55
58
  "lecore.Devices.PhaseDet" = ["*.json"]
@@ -1,261 +1,269 @@
1
- from __future__ import annotations
2
-
3
- import time
4
- from typing import Any, ClassVar, Dict, List, Optional, Union
5
-
6
- from ..TestFrame.TestUtils import *
7
- from ..LeBin.SerialCom import SerialCom
8
- from ..LeBin.RegisterMap import RegisterMap
9
- from ..LeBin.VisualLeBin import VisualLeBin
10
- from ..LeBin.UpgradeFirmware import UpgradeFirmware
11
- from .DeviceBase import DeviceBase
12
- from .Validation import (PathLike, ComSettings, UsbId, load_json, check_lebin_reg_map, check_reg_value, check_file,
13
- check_comport, check_baud_rate, check_parity, check_stop_bits, check_timeout)
14
-
15
-
16
- class LeBinDev(DeviceBase):
17
- """
18
- Generic LeBin device using proprietary LE register map and protocol.
19
-
20
- A new device is declared by class attributes, the configuration files are looked up in the directory
21
- of the module declaring the class::
22
-
23
- class MyDevice(LeBinDev):
24
- reg_map = "my_registers.json" # required
25
- usb = UsbId(vid=1155, pid=22336, sn="MY") # optional, used by search_com()
26
- reg_names = MyDeviceRegs # optional, checked against the register map
27
-
28
- Without subclassing, use ``LeBinDev(reg_map="path/to/map.json", config_dir="path/to/configs")``.
29
- """
30
- #: USB identification used by search_com()
31
- usb: ClassVar[UsbId] = UsbId()
32
-
33
- def __init_subclass__(cls, **kwargs):
34
- super().__init_subclass__(**kwargs)
35
- if not isinstance(cls.usb, UsbId):
36
- raise TypeError(f"{cls.__name__}.usb must be UsbId, got {type(cls.usb).__name__}")
37
-
38
- def __init__(self, com: Optional[PathLike] = None, reg_map: Optional[PathLike] = None, *,
39
- config_dir: Optional[PathLike] = None):
40
- """
41
- Initialize LeBin device
42
-
43
- The respective parameters allow to redefine default settings. In most cases, it is not necessary.
44
- Relative paths are resolved against current working directory, then against config_dir.
45
- :param com: Communication settings file override
46
- :param reg_map: Register map definition file override
47
- :param config_dir: Directory with default configuration files override
48
- """
49
- self.com = SerialCom()
50
- self.regs = RegisterMap(self.com)
51
- self._path = ""
52
- self._upg = None
53
- self._vis = None
54
- self._def_com = self.com_settings
55
- self._def_regs = None
56
- self._com_file = ""
57
- self._reg_file = ""
58
- self._def_usb = {'vid': self.usb.vid, 'pid': self.usb.pid, 'sn': self.usb.sn}
59
- self._regs_by_name: Dict[str, dict] = {}
60
-
61
- if not self._init_paths(config_dir, reg_map, type(self).__init__ is not LeBinDev.__init__):
62
- return
63
-
64
- # Initialize all internal structures
65
- self._com(com)
66
- self._reg_map(reg_map)
67
- self._upgrade()
68
- self._visual()
69
-
70
- def open(self, comport: Optional[str] = None, baud_rate: Optional[int] = None, parity: Optional[str] = None,
71
- stop_bits: Optional[Union[int, float]] = None, timeout: Optional[float] = None,
72
- client: Optional[SerialCom] = None) -> None:
73
- """
74
- Open COM port with optional redefinition of default properties.
75
-
76
- Any from the following may redefine default COM port property defined in default json files. For all defaults,
77
- just open() may be used.
78
- :param comport: Comport name, e.g., 'COM5'
79
- :param baud_rate: Baud rate speed
80
- :param parity: Parity, either 'N', 'E', 'O'
81
- :param stop_bits: Number of stop bits, 1, 1.5 or 2
82
- :param timeout: Timeout of communication response in seconds
83
- :param client: Use previously created communication object (SerialCom), e.g., for sharing a COM port
84
- :return: None
85
- """
86
- # Validate all parameters before changing anything
87
- if client is not None:
88
- if not hasattr(client, 'request_response'):
89
- raise TypeError(f"client must be SerialCom compatible object, got {type(client).__name__}")
90
- if any(p is not None for p in (comport, baud_rate, parity, stop_bits, timeout)):
91
- raise ValueError("COM port parameters cannot be combined with external client")
92
- if comport is not None:
93
- check_comport(comport)
94
- if baud_rate is not None:
95
- check_baud_rate(baud_rate)
96
- if parity is not None:
97
- parity = check_parity(parity)
98
- if stop_bits is not None:
99
- check_stop_bits(stop_bits)
100
- if timeout is not None:
101
- check_timeout(timeout)
102
-
103
- # External communication object
104
- if client is not None:
105
- self.com = client
106
- self.regs.com = client
107
- if self._upg is not None:
108
- self._upg.com = client
109
- return
110
-
111
- # Com port settings
112
- self.close()
113
- if comport is not None:
114
- self.com.comport = comport
115
- if baud_rate is not None:
116
- self.com.baud = baud_rate
117
- if parity is not None:
118
- self.com.parity = parity
119
- if stop_bits is not None:
120
- self.com.stop = stop_bits
121
- if timeout is not None:
122
- self.com.timeout = timeout
123
- self.com.reopen()
124
-
125
- def search_com(self) -> List[str]:
126
- """
127
- Search USB devices by VID, PID, and serial number a return a list of viable COM ports
128
- :return: List of COM ports
129
- """
130
- usb = UsbId(**self._def_usb)
131
- return self.com.get_list_ports(usb.vid, usb.pid, usb.sn)
132
-
133
- def read(self, name: str) -> Any:
134
- """
135
- Read register
136
- :param name: Register name
137
- :return: Register value on read success
138
- :return: None on read error
139
- """
140
- self._get_reg(name)
141
- return self.regs.read_by_name(name)
142
-
143
- def write(self, name: str, value: Any) -> Optional[bool]:
144
- """
145
- Write register. Value is checked against register format, Min/Max limits and enumeration.
146
- A list of values writes consecutive registers starting with the given one.
147
- :param name: Register name
148
- :param value: Register value or list of values
149
- :return: True on write success
150
- :return: None on write error
151
- """
152
- reg = self._get_reg(name)
153
- values = list(value) if isinstance(value, (list, tuple)) else [value]
154
- idx = self.regs.regs.index(reg)
155
- if idx + len(values) > len(self.regs.regs):
156
- raise ValueError(f"Writing {len(values)} values from register {name} exceeds the register map")
157
- for i, val in enumerate(values):
158
- r = self.regs.regs[idx + i]
159
- if str(r['Access']).startswith('RO'):
160
- raise ValueError(f"Register {r['Name']} is read-only")
161
- # LeBin FLOAT is a 32-bit float
162
- fmt = 'FLOAT32' if r['VarType'] == 'FLOAT' else r['VarType']
163
- check_reg_value(r, val, fmt, 8 * (1 << (r['Id'] & 7)), signed_ok=False)
164
- return self.regs.write_by_name(name, value)
165
-
166
- def close(self) -> None:
167
- """
168
- Close communication
169
- """
170
- self.com.close()
171
-
172
- def visual(self, timeout: Optional[float] = None, height: int = 800, width: int = 600):
173
- """
174
- Run visual modbus graphic application
175
- :param timeout: Timeout of automatic application shutdown in seconds, None for no timeout
176
- :param height: Height of window
177
- :param width: Width of window
178
- :return: True on exit
179
- """
180
- if timeout is not None:
181
- check_timeout(timeout)
182
- for n, v in (('height', height), ('width', width)):
183
- if not isinstance(v, int) or v <= 0:
184
- raise ValueError(f"{n} must be a positive integer, got {v!r}")
185
- self.close()
186
- self._vis.draw(height, width)
187
- return self._vis.handle(timeout)
188
-
189
- def upgrade_firmware(self, file: PathLike, reboot_time: float = 5) -> bool:
190
- """
191
- Upgrade firmware in the device. This function also checks the version of firmware after upgrade.
192
- :param file: Firmware file to upgrade
193
- :param reboot_time: Time device takes to reboot
194
- :return: Status of update, True on success, False on failure
195
- """
196
- check_file(file, "Firmware")
197
- if not isinstance(reboot_time, (int, float)) or reboot_time < 0:
198
- raise ValueError(f"reboot_time must be a non-negative number, got {reboot_time!r}")
199
- self._get_reg("FIRM_REVISION")
200
- ver_orig = get_firmware_version(file, 512)
201
-
202
- # Run upgrade procedure
203
- self._upg.load_file(file)
204
- ret = self._upg.run()
205
- self.com.close()
206
- # Sleep reboot time
207
- time.sleep(reboot_time)
208
- self.com.reopen()
209
- # Check status and version after upgrade
210
- ver = self.regs.read_by_name("FIRM_REVISION")
211
-
212
- return ret and ver == ver_orig
213
-
214
- def send_file(self, file: PathLike) -> bool:
215
- """
216
- Send generic file to device
217
- :param file: File to send
218
- :return: Status of update, True on success, False on failure
219
- """
220
- check_file(file, "File to send")
221
- # Run upgrade procedure
222
- self._upg.load_file(file)
223
- ret = self._upg.run()
224
- return ret == 0
225
-
226
- def _visual(self) -> None:
227
- """
228
- Initialize VisualModbus graphic application
229
- """
230
- self._vis = VisualLeBin(self.regs)
231
-
232
- def _upgrade(self) -> None:
233
- """
234
- Initialize upgrade module
235
- """
236
- self._upg = UpgradeFirmware(self.com)
237
-
238
- def _com(self, settings: Optional[PathLike]) -> None:
239
- """
240
- Initialize communication module
241
- :param settings: Communication settings file
242
- """
243
- self._com_file = self._path_complete(settings, self._def_com, "Communication settings")
244
- ComSettings.from_json(self._com_file)
245
- self.com.open(self._com_file)
246
-
247
- def _reg_map(self, reg_map: Optional[PathLike]) -> None:
248
- """
249
- Initialize register map module
250
- :param reg_map: Register map definition json file
251
- """
252
- self._reg_file = self._path_complete(reg_map, self._def_regs, "Register map")
253
- check_lebin_reg_map(load_json(self._reg_file, "Register map"), self._reg_file)
254
- self.regs.load(self._reg_file)
255
- self._regs_by_name = {r['Name']: r for r in self.regs.regs}
256
- self._check_reg_names()
257
-
258
- def _reg_dict(self) -> Dict[str, dict]:
259
- if not self._regs_by_name:
260
- raise RuntimeError(f"{type(self).__name__}: register map is not loaded")
261
- return self._regs_by_name
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any, ClassVar, Dict, List, Optional, Union
5
+
6
+ import serial
7
+
8
+ from ..TestFrame.TestUtils import *
9
+ from ..LeBin.SerialCom import SerialCom
10
+ from ..LeBin.RegisterMap import RegisterMap
11
+ from ..LeBin.VisualLeBin import VisualLeBin
12
+ from ..LeBin.UpgradeFirmware import UpgradeFirmware
13
+ from .DeviceBase import DeviceBase
14
+ from .Validation import (PathLike, ComSettings, UsbId, load_json, check_lebin_reg_map, check_reg_value, check_file,
15
+ check_comport, check_baud_rate, check_parity, check_stop_bits, check_timeout)
16
+
17
+
18
+ class LeBinDev(DeviceBase):
19
+ """
20
+ Generic LeBin device using proprietary LE register map and protocol.
21
+
22
+ A new device is declared by class attributes, the configuration files are looked up in the directory
23
+ of the module declaring the class::
24
+
25
+ class MyDevice(LeBinDev):
26
+ reg_map = "my_registers.json" # required
27
+ usb = UsbId(vid=1155, pid=22336, sn="MY") # optional, used by search_com()
28
+ reg_names = MyDeviceRegs # optional, checked against the register map
29
+
30
+ Without subclassing, use ``LeBinDev(reg_map="path/to/map.json", config_dir="path/to/configs")``.
31
+ """
32
+ #: USB identification used by search_com()
33
+ usb: ClassVar[UsbId] = UsbId()
34
+
35
+ def __init_subclass__(cls, **kwargs):
36
+ super().__init_subclass__(**kwargs)
37
+ if not isinstance(cls.usb, UsbId):
38
+ raise TypeError(f"{cls.__name__}.usb must be UsbId, got {type(cls.usb).__name__}")
39
+
40
+ def __init__(self, com: Optional[PathLike] = None, reg_map: Optional[PathLike] = None, *,
41
+ config_dir: Optional[PathLike] = None):
42
+ """
43
+ Initialize LeBin device
44
+
45
+ The respective parameters allow to redefine default settings. In most cases, it is not necessary.
46
+ Relative paths are resolved against current working directory, then against config_dir.
47
+ :param com: Communication settings file override
48
+ :param reg_map: Register map definition file override
49
+ :param config_dir: Directory with default configuration files override
50
+ """
51
+ self.com = SerialCom()
52
+ self.regs = RegisterMap(self.com)
53
+ self._path = ""
54
+ self._upg = None
55
+ self._vis = None
56
+ self._def_com = self.com_settings
57
+ self._def_regs = None
58
+ self._com_file = ""
59
+ self._reg_file = ""
60
+ self._def_usb = {'vid': self.usb.vid, 'pid': self.usb.pid, 'sn': self.usb.sn}
61
+ self._regs_by_name: Dict[str, dict] = {}
62
+
63
+ if not self._init_paths(config_dir, reg_map, type(self).__init__ is not LeBinDev.__init__):
64
+ return
65
+
66
+ # Initialize all internal structures
67
+ self._com(com)
68
+ self._reg_map(reg_map)
69
+ self._upgrade()
70
+ self._visual()
71
+
72
+ def open(self, comport: Optional[str] = None, baud_rate: Optional[int] = None, parity: Optional[str] = None,
73
+ stop_bits: Optional[Union[int, float]] = None, timeout: Optional[float] = None,
74
+ client: Optional[SerialCom] = None) -> None:
75
+ """
76
+ Open COM port with optional redefinition of default properties.
77
+
78
+ Any from the following may redefine default COM port property defined in default json files. For all defaults,
79
+ just open() may be used.
80
+ :param comport: Comport name, e.g., 'COM5'
81
+ :param baud_rate: Baud rate speed
82
+ :param parity: Parity, either 'N', 'E', 'O'
83
+ :param stop_bits: Number of stop bits, 1, 1.5 or 2
84
+ :param timeout: Timeout of communication response in seconds
85
+ :param client: Use previously created communication object (SerialCom), e.g., for sharing a COM port
86
+ :return: None
87
+ """
88
+ # Validate all parameters before changing anything
89
+ if client is not None:
90
+ if not hasattr(client, 'request_response'):
91
+ raise TypeError(f"client must be SerialCom compatible object, got {type(client).__name__}")
92
+ if any(p is not None for p in (comport, baud_rate, parity, stop_bits, timeout)):
93
+ raise ValueError("COM port parameters cannot be combined with external client")
94
+ if comport is not None:
95
+ check_comport(comport)
96
+ if baud_rate is not None:
97
+ check_baud_rate(baud_rate)
98
+ if parity is not None:
99
+ parity = check_parity(parity)
100
+ if stop_bits is not None:
101
+ check_stop_bits(stop_bits)
102
+ if timeout is not None:
103
+ check_timeout(timeout)
104
+
105
+ # External communication object
106
+ if client is not None:
107
+ self.com = client
108
+ self.regs.com = client
109
+ if self._upg is not None:
110
+ self._upg.com = client
111
+ return
112
+
113
+ # Com port settings
114
+ self.close()
115
+ if comport is not None:
116
+ self.com.comport = comport
117
+ if baud_rate is not None:
118
+ self.com.baud = baud_rate
119
+ if parity is not None:
120
+ self.com.parity = parity
121
+ if stop_bits is not None:
122
+ self.com.stop = stop_bits
123
+ if timeout is not None:
124
+ self.com.timeout = timeout
125
+ self.com.reopen()
126
+
127
+ def search_com(self) -> List[str]:
128
+ """
129
+ Search USB devices by VID, PID, and serial number a return a list of viable COM ports
130
+ :return: List of COM ports
131
+ """
132
+ usb = UsbId(**self._def_usb)
133
+ return self.com.get_list_ports(usb.vid, usb.pid, usb.sn)
134
+
135
+ def read(self, name: str) -> Any:
136
+ """
137
+ Read register
138
+ :param name: Register name
139
+ :return: Register value on read success
140
+ :return: None on read error
141
+ """
142
+ self._get_reg(name)
143
+ try:
144
+ return self.regs.read_by_name(name)
145
+ except serial.SerialException:
146
+ return None
147
+
148
+ def write(self, name: str, value: Any) -> Optional[bool]:
149
+ """
150
+ Write register. Value is checked against register format, Min/Max limits and enumeration.
151
+ A list of values writes consecutive registers starting with the given one.
152
+ :param name: Register name
153
+ :param value: Register value or list of values
154
+ :return: True on write success
155
+ :return: None on write error
156
+ """
157
+ reg = self._get_reg(name)
158
+ values = list(value) if isinstance(value, (list, tuple)) else [value]
159
+ idx = self.regs.regs.index(reg)
160
+ if idx + len(values) > len(self.regs.regs):
161
+ raise ValueError(f"Writing {len(values)} values from register {name} exceeds the register map")
162
+ for i, val in enumerate(values):
163
+ r = self.regs.regs[idx + i]
164
+ if str(r['Access']).startswith('RO'):
165
+ raise ValueError(f"Register {r['Name']} is read-only")
166
+ # LeBin FLOAT is a 32-bit float
167
+ fmt = 'FLOAT32' if r['VarType'] == 'FLOAT' else r['VarType']
168
+ check_reg_value(r, val, fmt, 8 * (1 << (r['Id'] & 7)), signed_ok=False)
169
+ try:
170
+ return self.regs.write_by_name(name, value)
171
+ except serial.SerialException:
172
+ return None
173
+
174
+ def close(self) -> None:
175
+ """
176
+ Close communication
177
+ """
178
+ self.com.close()
179
+
180
+ def visual(self, timeout: Optional[float] = None, height: int = 800, width: int = 600):
181
+ """
182
+ Run visual modbus graphic application
183
+ :param timeout: Timeout of automatic application shutdown in seconds, None for no timeout
184
+ :param height: Height of window
185
+ :param width: Width of window
186
+ :return: True on exit
187
+ """
188
+ if timeout is not None:
189
+ check_timeout(timeout)
190
+ for n, v in (('height', height), ('width', width)):
191
+ if not isinstance(v, int) or v <= 0:
192
+ raise ValueError(f"{n} must be a positive integer, got {v!r}")
193
+ self.close()
194
+ self._vis.draw(height, width)
195
+ return self._vis.handle(timeout)
196
+
197
+ def upgrade_firmware(self, file: PathLike, reboot_time: float = 5) -> bool:
198
+ """
199
+ Upgrade firmware in the device. This function also checks the version of firmware after upgrade.
200
+ :param file: Firmware file to upgrade
201
+ :param reboot_time: Time device takes to reboot
202
+ :return: Status of update, True on success, False on failure
203
+ """
204
+ check_file(file, "Firmware")
205
+ if not isinstance(reboot_time, (int, float)) or reboot_time < 0:
206
+ raise ValueError(f"reboot_time must be a non-negative number, got {reboot_time!r}")
207
+ self._get_reg("FIRM_REVISION")
208
+ ver_orig = get_firmware_version(file, 512)
209
+
210
+ # Run upgrade procedure
211
+ self._upg.load_file(file)
212
+ ret = self._upg.run()
213
+ self.com.close()
214
+ # Sleep reboot time
215
+ time.sleep(reboot_time)
216
+ self.com.reopen()
217
+ # Check status and version after upgrade
218
+ ver = self.regs.read_by_name("FIRM_REVISION")
219
+
220
+ return ret and ver == ver_orig
221
+
222
+ def send_file(self, file: PathLike) -> bool:
223
+ """
224
+ Send generic file to device
225
+ :param file: File to send
226
+ :return: Status of update, True on success, False on failure
227
+ """
228
+ check_file(file, "File to send")
229
+ # Run upgrade procedure
230
+ self._upg.load_file(file)
231
+ # LeBin upgrade returns True on success (unlike Modbus return code 0)
232
+ return bool(self._upg.run())
233
+
234
+ def _visual(self) -> None:
235
+ """
236
+ Initialize VisualModbus graphic application
237
+ """
238
+ self._vis = VisualLeBin(self.regs)
239
+
240
+ def _upgrade(self) -> None:
241
+ """
242
+ Initialize upgrade module
243
+ """
244
+ self._upg = UpgradeFirmware(self.com)
245
+
246
+ def _com(self, settings: Optional[PathLike]) -> None:
247
+ """
248
+ Initialize communication module
249
+ :param settings: Communication settings file
250
+ """
251
+ self._com_file = self._path_complete(settings, self._def_com, "Communication settings")
252
+ ComSettings.from_json(self._com_file)
253
+ self.com.open(self._com_file)
254
+
255
+ def _reg_map(self, reg_map: Optional[PathLike]) -> None:
256
+ """
257
+ Initialize register map module
258
+ :param reg_map: Register map definition json file
259
+ """
260
+ self._reg_file = self._path_complete(reg_map, self._def_regs, "Register map")
261
+ check_lebin_reg_map(load_json(self._reg_file, "Register map"), self._reg_file)
262
+ self.regs.load(self._reg_file)
263
+ self._regs_by_name = {r['Name']: r for r in self.regs.regs}
264
+ self._check_reg_names()
265
+
266
+ def _reg_dict(self) -> Dict[str, dict]:
267
+ if not self._regs_by_name:
268
+ raise RuntimeError(f"{type(self).__name__}: register map is not loaded")
269
+ return self._regs_by_name