lecore 0.2.2__tar.gz → 0.2.3__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.
- {lecore-0.2.2/src/lecore.egg-info → lecore-0.2.3}/PKG-INFO +1 -1
- {lecore-0.2.2 → lecore-0.2.3}/pyproject.toml +1 -1
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/LeBinDev.py +271 -269
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/LeBin/SerialCom.py +3 -2
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/LeBin/VisualLeBin.py +2 -1
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Scaffold.py +9 -1
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/AppLogging.py +3 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/HelpAbout.py +1 -7
- lecore-0.2.3/src/lecore/_version.py +7 -0
- {lecore-0.2.2 → lecore-0.2.3/src/lecore.egg-info}/PKG-INFO +1 -1
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore.egg-info/SOURCES.txt +1 -0
- {lecore-0.2.2 → lecore-0.2.3}/LICENSE +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/MANIFEST.in +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/README.md +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/setup.cfg +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/DeviceBase.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/ModbusDev.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/PhaseDet/ComSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/PhaseDet/PhaseDet.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/PhaseDet/PhaseDetRegs.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/PhaseDet/PhaseDet_Modbus.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/PhaseDet/UpgradeSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/PhaseDet/VisualSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/PhaseDet/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/Rftb/ComSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/Rftb/Rftb.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/Rftb/RftbLeBin.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/Rftb/UpgradeSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/Rftb/VisualSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/Rftb/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/Rftb/rftbRegs.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/Rftb/rftb_Modbus.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/Rftb/rftb_registers.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/RtdEmul/ComSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/RtdEmul/RtdEmul.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/RtdEmul/RtdEmulRegs.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/RtdEmul/RtdEmul_Modbus.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/RtdEmul/UpgradeSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/RtdEmul/VisualSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/RtdEmul/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/Validation.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/VmsRtd/ComSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/VmsRtd/UpgradeSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/VmsRtd/VMS_RTDRegs.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/VmsRtd/VMS_RTD_Modbus.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/VmsRtd/VisualSettings.json +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/VmsRtd/VmsRtd.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/VmsRtd/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Devices/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Doc/ModbusTable.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Doc/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Fg320/Fg320.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Fg320/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/LeBin/RegisterMap.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/LeBin/UpgradeFirmware.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/LeBin/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/Looger.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/QUICKSTART.md +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/TestFrame/CoreTest.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/TestFrame/ModbusUnitTest.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/TestFrame/RtuClient.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/TestFrame/Runner.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/TestFrame/TestUtils.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/TestFrame/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/Crc32.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/MbClient.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/MbUpgrade.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/ReadWrite.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/RegMap.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/TextMbApp.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/TextMbAppCss.tcss +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/VisualMbApp.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/VmSettings.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/VisualModbus/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/__init__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/__main__.py +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore/py.typed +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore.egg-info/dependency_links.txt +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore.egg-info/entry_points.txt +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore.egg-info/requires.txt +0 -0
- {lecore-0.2.2 → lecore-0.2.3}/src/lecore.egg-info/top_level.txt +0 -0
|
@@ -1,269 +1,271 @@
|
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
self.
|
|
263
|
-
self.
|
|
264
|
-
self.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
# A device with USB identification is looked up by it, the com port in settings is only a fallback
|
|
254
|
+
ports = self.search_com() if self.usb.vid else []
|
|
255
|
+
self.com.open(self._com_file, ports[0] if ports else None)
|
|
256
|
+
|
|
257
|
+
def _reg_map(self, reg_map: Optional[PathLike]) -> None:
|
|
258
|
+
"""
|
|
259
|
+
Initialize register map module
|
|
260
|
+
:param reg_map: Register map definition json file
|
|
261
|
+
"""
|
|
262
|
+
self._reg_file = self._path_complete(reg_map, self._def_regs, "Register map")
|
|
263
|
+
check_lebin_reg_map(load_json(self._reg_file, "Register map"), self._reg_file)
|
|
264
|
+
self.regs.load(self._reg_file)
|
|
265
|
+
self._regs_by_name = {r['Name']: r for r in self.regs.regs}
|
|
266
|
+
self._check_reg_names()
|
|
267
|
+
|
|
268
|
+
def _reg_dict(self) -> Dict[str, dict]:
|
|
269
|
+
if not self._regs_by_name:
|
|
270
|
+
raise RuntimeError(f"{type(self).__name__}: register map is not loaded")
|
|
271
|
+
return self._regs_by_name
|
|
@@ -18,17 +18,18 @@ class SerialCom:
|
|
|
18
18
|
self.timeout = 1.0
|
|
19
19
|
self.comport = None
|
|
20
20
|
|
|
21
|
-
def open(self, filename):
|
|
21
|
+
def open(self, filename, comport=None):
|
|
22
22
|
"""
|
|
23
23
|
Open com port with given communication settings
|
|
24
24
|
:param filename: Json settings of COM port
|
|
25
|
+
:param comport: Com port name overriding the one from settings, None to use settings
|
|
25
26
|
:return: None
|
|
26
27
|
"""
|
|
27
28
|
with io.open(filename, 'r', encoding='utf-8-sig') as f:
|
|
28
29
|
s = json.load(f)
|
|
29
30
|
self.settings = s
|
|
30
31
|
|
|
31
|
-
self.comport = s['comport']
|
|
32
|
+
self.comport = s['comport'] if comport is None else comport
|
|
32
33
|
self.baud = s['baud_rate']
|
|
33
34
|
self.stop = s['stop_bits']
|
|
34
35
|
self.parity = s['parity']
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from .RegisterMap import *
|
|
3
|
+
from .._version import __version__
|
|
3
4
|
try:
|
|
4
5
|
import FreeSimpleGUI as SG
|
|
5
6
|
except ImportError:
|
|
@@ -60,7 +61,7 @@ class VisualLeBin:
|
|
|
60
61
|
SG.Text("RSSI"), SG.Input('-1', size=(4, 1), key='T_RSSI')])
|
|
61
62
|
layout.append([SG.Text('Status', key='T_STATUS', size=(width // 8, None))])
|
|
62
63
|
|
|
63
|
-
self.window = SG.Window('LeBin visual application. Version:
|
|
64
|
+
self.window = SG.Window('LeBin visual application. Version: ' + __version__, layout, resizable=True, finalize=True)
|
|
64
65
|
self.update()
|
|
65
66
|
except AttributeError:
|
|
66
67
|
raise ImportError("FreeSimpleGUI is probably missing")
|
|
@@ -355,7 +355,9 @@ def new_device(name: str, protocol: Optional[str] = None, directory: PathLike =
|
|
|
355
355
|
f"reg_names of {cls_name} is not declared", UserWarning, stacklevel=2)
|
|
356
356
|
|
|
357
357
|
# Configuration files, existing ones are kept
|
|
358
|
-
|
|
358
|
+
# Com port, baud rate and parity apply to the Modbus device, LeBin of a dual device keeps the defaults
|
|
359
|
+
com = {} if dual and proto == 'lebin' else dict(comport=comport, baud_rate=baud_rate, parity=parity)
|
|
360
|
+
configs = {com_file: dict(_COM_SETTINGS, **com)}
|
|
359
361
|
if proto == 'modbus':
|
|
360
362
|
configs['UpgradeSettings.json'] = _UPGRADE_SETTINGS
|
|
361
363
|
configs['VisualSettings.json'] = dict(_VISUAL_SETTINGS, slave_address=slave, reg_map=map_file)
|
|
@@ -375,6 +377,12 @@ def new_device(name: str, protocol: Optional[str] = None, directory: PathLike =
|
|
|
375
377
|
init = os.path.join(directory, '__init__.py')
|
|
376
378
|
if os.path.exists(init):
|
|
377
379
|
result[init] = 'kept'
|
|
380
|
+
with open(init, encoding='utf-8') as f:
|
|
381
|
+
init_text = f.read()
|
|
382
|
+
absent = [d[0] for d in devices if not re.search(rf"import\s+(?:.*[\s,])?{re.escape(d[0])}(?![\w])", init_text)]
|
|
383
|
+
if absent:
|
|
384
|
+
warnings.warn(f"{init} is kept and does not export {', '.join(absent)}: 'from package import {absent[0]}' "
|
|
385
|
+
f"returns the submodule, add 'from .{absent[0]} import {absent[0]}'", stacklevel=2)
|
|
378
386
|
else:
|
|
379
387
|
text = "".join(f"from .{d[0]} import {d[0]}\n" for d in devices)
|
|
380
388
|
if regs_found:
|
|
@@ -14,6 +14,7 @@ class LastRecordHandler(logging.Handler):
|
|
|
14
14
|
Initialize logging handler and clear all records
|
|
15
15
|
"""
|
|
16
16
|
logging.Handler.__init__(self)
|
|
17
|
+
self.formatter = logging.Formatter()
|
|
17
18
|
self.records = []
|
|
18
19
|
|
|
19
20
|
def emit(self, record):
|
|
@@ -22,6 +23,8 @@ class LastRecordHandler(logging.Handler):
|
|
|
22
23
|
:param record: Current record
|
|
23
24
|
:return: None
|
|
24
25
|
"""
|
|
26
|
+
# asctime is set only by Formatter.format(), which this handler does not call
|
|
27
|
+
record.asctime = self.formatter.formatTime(record)
|
|
25
28
|
self.records.append(record)
|
|
26
29
|
if len(self.records) > self.MAX_COUNT:
|
|
27
30
|
self.records.pop(0)
|
|
@@ -7,13 +7,7 @@ except ImportError:
|
|
|
7
7
|
except ImportError:
|
|
8
8
|
import datetime as SG
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
VERSION_MINOR = 1
|
|
13
|
-
|
|
14
|
-
VERSION_BUILD = 8
|
|
15
|
-
|
|
16
|
-
VERSION = "{}.{}.{}".format(VERSION_MAJOR, VERSION_MINOR, VERSION_BUILD)
|
|
10
|
+
from .._version import __version__ as VERSION
|
|
17
11
|
|
|
18
12
|
RELEASE_NOTE_9 = "2020/01/31 - 0.1.8 - \n"\
|
|
19
13
|
" - Fix podpory čtení a zápisu stringů \n" \
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Package version, taken from the installed distribution metadata (single source: pyproject.toml)."""
|
|
2
|
+
from importlib import metadata
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
__version__ = metadata.version("lecore")
|
|
6
|
+
except metadata.PackageNotFoundError: # running from a source tree without installation
|
|
7
|
+
__version__ = "unknown"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|