riden 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- riden/__init__.py +26 -0
- riden/bt/__init__.py +5 -0
- riden/bt/bt_serial.py +204 -0
- riden/bt/protocol_bt.py +48 -0
- riden/enums.py +11 -0
- riden/field.py +204 -0
- riden/model.py +32 -0
- riden/py.typed +0 -0
- riden/riden.py +252 -0
- riden/transport/__init__.py +17 -0
- riden/transport/delayed.py +46 -0
- riden/transport/errors.py +44 -0
- riden/transport/factory.py +54 -0
- riden/transport/keepalive.py +160 -0
- riden/transport/logging.py +93 -0
- riden/transport/modbus.py +107 -0
- riden/transport/reconnectable.py +114 -0
- riden/transport/transport.py +57 -0
- riden-0.1.0.dist-info/METADATA +168 -0
- riden-0.1.0.dist-info/RECORD +22 -0
- riden-0.1.0.dist-info/WHEEL +4 -0
- riden-0.1.0.dist-info/licenses/LICENSE +21 -0
riden/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import serial
|
|
2
|
+
|
|
3
|
+
from .bt import BtSerial
|
|
4
|
+
from .enums import Mode, Protection
|
|
5
|
+
from .field import Field
|
|
6
|
+
from .riden import Riden
|
|
7
|
+
from .transport import Delayed, KeepAlive, Logging, Modbus, Reconnectable, Transport
|
|
8
|
+
|
|
9
|
+
HANDLER_PACKAGE = "riden.bt"
|
|
10
|
+
|
|
11
|
+
if HANDLER_PACKAGE not in serial.protocol_handler_packages:
|
|
12
|
+
serial.protocol_handler_packages.append(HANDLER_PACKAGE)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"BtSerial",
|
|
16
|
+
"Delayed",
|
|
17
|
+
"Field",
|
|
18
|
+
"KeepAlive",
|
|
19
|
+
"Logging",
|
|
20
|
+
"Modbus",
|
|
21
|
+
"Mode",
|
|
22
|
+
"Protection",
|
|
23
|
+
"Reconnectable",
|
|
24
|
+
"Riden",
|
|
25
|
+
"Transport",
|
|
26
|
+
]
|
riden/bt/__init__.py
ADDED
riden/bt/bt_serial.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Bluetooth SPP transport shaped like a pyserial port.
|
|
2
|
+
|
|
3
|
+
BtSerial.port is always a Bluetooth MAC, just as serial.Serial.port is a
|
|
4
|
+
device path. URL parsing lives only in serial_urlhandler/protocol_bt.py,
|
|
5
|
+
which pyserial uses via serial_for_url():
|
|
6
|
+
|
|
7
|
+
serial.serial_for_url("bt://E2:F9:C6:24:71:C8?channel=1&timeout=0.5")
|
|
8
|
+
|
|
9
|
+
Direct construction uses the MAC:
|
|
10
|
+
|
|
11
|
+
BtSerial("E2:F9:C6:24:71:C8", channel=1, timeout=0.5)
|
|
12
|
+
|
|
13
|
+
The connect timeout is an internal constant (CONNECT_TIMEOUT), not part of the
|
|
14
|
+
serial interface. For a configured but closed object use
|
|
15
|
+
serial_for_url(..., do_not_open=True), or BtSerial(None) and set .port before
|
|
16
|
+
open().
|
|
17
|
+
"""
|
|
18
|
+
import contextlib
|
|
19
|
+
import re
|
|
20
|
+
import select
|
|
21
|
+
import socket
|
|
22
|
+
import time
|
|
23
|
+
from typing import override
|
|
24
|
+
|
|
25
|
+
import serial
|
|
26
|
+
|
|
27
|
+
# Fixed for minimalmodbus; RFCOMM has no real baudrate.
|
|
28
|
+
BAUDRATE = 115200
|
|
29
|
+
|
|
30
|
+
_MAC_RE = re.compile(r"^(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$")
|
|
31
|
+
|
|
32
|
+
def parse_mac(value: str) -> str:
|
|
33
|
+
value = value.strip()
|
|
34
|
+
if not _MAC_RE.match(value):
|
|
35
|
+
raise ValueError(f"invalid Bluetooth MAC: {value!r}")
|
|
36
|
+
return value.replace("-", ":").upper()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BtSerial(contextlib.AbstractContextManager["BtSerial"]):
|
|
40
|
+
|
|
41
|
+
def __init__(self,
|
|
42
|
+
port: str | None = None,
|
|
43
|
+
channel: int = 1,
|
|
44
|
+
timeout: float | None = 0.5,
|
|
45
|
+
connect_timeout: float | None = 10
|
|
46
|
+
):
|
|
47
|
+
self.port = port
|
|
48
|
+
self.channel = channel
|
|
49
|
+
self.sock: socket.socket | None = None
|
|
50
|
+
self._buf = bytearray()
|
|
51
|
+
self.timeout = timeout
|
|
52
|
+
self.connect_timeout = connect_timeout
|
|
53
|
+
if port is not None:
|
|
54
|
+
self.open()
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def timeout(self) -> float | None:
|
|
58
|
+
return self._timeout
|
|
59
|
+
|
|
60
|
+
@timeout.setter
|
|
61
|
+
def timeout(self, value: float | None) -> None:
|
|
62
|
+
if value is not None:
|
|
63
|
+
try:
|
|
64
|
+
value + 1 # numeric check, like pyserial
|
|
65
|
+
except TypeError:
|
|
66
|
+
raise ValueError(f"Not a valid timeout: {value!r}") from None
|
|
67
|
+
if value < 0:
|
|
68
|
+
raise ValueError(f"Not a valid timeout: {value!r}")
|
|
69
|
+
self._timeout = value
|
|
70
|
+
if self.sock is not None:
|
|
71
|
+
self.sock.settimeout(value)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def baudrate(self) -> int:
|
|
75
|
+
# minimalmodbus reads this to calculate the 3.5-character silent
|
|
76
|
+
# period; RFCOMM itself has no baudrate setting.
|
|
77
|
+
return BAUDRATE
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_open(self) -> bool:
|
|
81
|
+
return self.sock is not None
|
|
82
|
+
|
|
83
|
+
# -- pyserial-compatible lifecycle ------------------------------------
|
|
84
|
+
def ensure_opened(self) -> socket.socket:
|
|
85
|
+
sock = self.sock
|
|
86
|
+
if sock is None:
|
|
87
|
+
raise serial.SerialException("Bluetooth socket is closed")
|
|
88
|
+
return sock
|
|
89
|
+
|
|
90
|
+
def open(self) -> None:
|
|
91
|
+
if self.is_open:
|
|
92
|
+
raise serial.SerialException("Port is already open.")
|
|
93
|
+
if self.port is None:
|
|
94
|
+
raise serial.SerialException("Port must be configured before it can be used.")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
mac = parse_mac(str(self.port))
|
|
98
|
+
except ValueError as exc:
|
|
99
|
+
raise serial.SerialException(str(exc)) from exc
|
|
100
|
+
channel = int(self.channel)
|
|
101
|
+
if not 1 <= channel <= 30:
|
|
102
|
+
raise ValueError(f"Bluetooth channel must be 1..30, got {channel}")
|
|
103
|
+
|
|
104
|
+
sock = socket.socket(
|
|
105
|
+
socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM
|
|
106
|
+
)
|
|
107
|
+
try:
|
|
108
|
+
sock.settimeout(self.connect_timeout)
|
|
109
|
+
sock.connect((mac, channel))
|
|
110
|
+
sock.settimeout(self.timeout)
|
|
111
|
+
except Exception:
|
|
112
|
+
sock.close()
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
self.sock = sock
|
|
116
|
+
self.mac = mac
|
|
117
|
+
self.channel = channel
|
|
118
|
+
self._buf.clear()
|
|
119
|
+
|
|
120
|
+
def close(self) -> None:
|
|
121
|
+
sock = self.sock
|
|
122
|
+
self.sock = None
|
|
123
|
+
if sock is not None:
|
|
124
|
+
sock.close()
|
|
125
|
+
|
|
126
|
+
def __del__(self):
|
|
127
|
+
with contextlib.suppress(Exception):
|
|
128
|
+
self.close()
|
|
129
|
+
|
|
130
|
+
@override
|
|
131
|
+
def __exit__(self, *_):
|
|
132
|
+
self.close()
|
|
133
|
+
|
|
134
|
+
# -- pyserial-like I/O ------------------------------------------------
|
|
135
|
+
def write(self, data: bytes) -> int:
|
|
136
|
+
sock = self.ensure_opened()
|
|
137
|
+
sock.sendall(bytes(data))
|
|
138
|
+
return len(data)
|
|
139
|
+
|
|
140
|
+
def read(self, size: int = 1) -> bytes:
|
|
141
|
+
sock = self.ensure_opened()
|
|
142
|
+
if size <= 0:
|
|
143
|
+
return b""
|
|
144
|
+
|
|
145
|
+
if self.timeout is None:
|
|
146
|
+
# pyserial semantics: None means blocking until size bytes or EOF.
|
|
147
|
+
while len(self._buf) < size:
|
|
148
|
+
chunk = sock.recv(size - len(self._buf))
|
|
149
|
+
if not chunk:
|
|
150
|
+
raise serial.SerialException("Bluetooth peer closed the link")
|
|
151
|
+
self._buf.extend(chunk)
|
|
152
|
+
out = bytes(self._buf[:size])
|
|
153
|
+
del self._buf[:size]
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
if self.timeout == 0:
|
|
157
|
+
# pyserial semantics: return immediately with what is available.
|
|
158
|
+
try:
|
|
159
|
+
chunk = sock.recv(size - len(self._buf), socket.MSG_DONTWAIT)
|
|
160
|
+
if chunk:
|
|
161
|
+
self._buf.extend(chunk)
|
|
162
|
+
except BlockingIOError:
|
|
163
|
+
pass
|
|
164
|
+
except OSError as exc:
|
|
165
|
+
raise serial.SerialException(str(exc)) from exc
|
|
166
|
+
out = bytes(self._buf[:size])
|
|
167
|
+
del self._buf[:size]
|
|
168
|
+
return out
|
|
169
|
+
|
|
170
|
+
# timeout > 0: the timeout is for the whole call (pyserial semantics).
|
|
171
|
+
# select() waits for readability with the time left, so the socket
|
|
172
|
+
# timeout is never touched and recv() cannot block past the deadline.
|
|
173
|
+
deadline = time.monotonic() + self.timeout
|
|
174
|
+
while len(self._buf) < size:
|
|
175
|
+
remaining = deadline - time.monotonic()
|
|
176
|
+
if remaining <= 0:
|
|
177
|
+
break
|
|
178
|
+
if not select.select([sock], [], [], remaining)[0]:
|
|
179
|
+
break
|
|
180
|
+
chunk = sock.recv(size - len(self._buf))
|
|
181
|
+
if not chunk:
|
|
182
|
+
raise serial.SerialException("Bluetooth peer closed the link")
|
|
183
|
+
self._buf.extend(chunk)
|
|
184
|
+
|
|
185
|
+
out = bytes(self._buf[:size])
|
|
186
|
+
del self._buf[:size]
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
def reset_input_buffer(self) -> None:
|
|
190
|
+
sock = self.ensure_opened()
|
|
191
|
+
self._buf.clear()
|
|
192
|
+
# Zero-timeout poll: recv() after a readable select() returns at once,
|
|
193
|
+
# so the socket timeout is never touched.
|
|
194
|
+
while select.select([sock], [], [], 0)[0]:
|
|
195
|
+
if not sock.recv(4096):
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
def reset_output_buffer(self) -> None:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
def flush(self) -> None:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
|
riden/bt/protocol_bt.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
import typing
|
|
3
|
+
from functools import partial
|
|
4
|
+
from urllib.parse import parse_qs, urlsplit
|
|
5
|
+
|
|
6
|
+
from .bt_serial import BtSerial
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_bt_url(url: str):
|
|
10
|
+
parsed = urlsplit(url)
|
|
11
|
+
if parsed.scheme.lower() != "bt":
|
|
12
|
+
raise ValueError(f"not a bt:// URL: {url!r}")
|
|
13
|
+
|
|
14
|
+
mac = parsed.netloc
|
|
15
|
+
if not mac:
|
|
16
|
+
raise ValueError(f"no MAC in bt URL: {url!r}")
|
|
17
|
+
if parsed.path not in ("", "/"):
|
|
18
|
+
raise ValueError(
|
|
19
|
+
"channel must be a query parameter, e.g. bt://MAC?channel=1"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
query = parse_qs(parsed.query)
|
|
23
|
+
|
|
24
|
+
def qs(name: str, type: type):
|
|
25
|
+
values = query.get(name)
|
|
26
|
+
if values is None:
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
return type(values[0])
|
|
30
|
+
except (TypeError, ValueError) as exc:
|
|
31
|
+
raise ValueError(f"invalid {name} in URL: {values[0]!r}") from exc
|
|
32
|
+
|
|
33
|
+
kwargs: dict[str, typing.Any] = {
|
|
34
|
+
"channel": qs("channel", int) or 1,
|
|
35
|
+
"timeout": qs("timeout", float),
|
|
36
|
+
"connect_timeout": qs("connect_timeout", float)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return mac, kwargs
|
|
40
|
+
|
|
41
|
+
def serial_class_for_url(url: str):
|
|
42
|
+
"""Return (MAC, partial(BtSerial, channel=..., timeout=...)).
|
|
43
|
+
|
|
44
|
+
pyserial calls this hook, then constructs the class with port=None, sets
|
|
45
|
+
instance.port to the returned MAC, and calls open() unless do_not_open.
|
|
46
|
+
"""
|
|
47
|
+
mac, kwargs = parse_bt_url(url)
|
|
48
|
+
return mac, partial(BtSerial, **kwargs)
|
riden/enums.py
ADDED
riden/field.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from functools import partial
|
|
4
|
+
from typing import Any, override
|
|
5
|
+
|
|
6
|
+
from .enums import Mode, Protection
|
|
7
|
+
from .model import Model
|
|
8
|
+
|
|
9
|
+
type RegRead = Callable[[tuple[int, ...], Model], Any]
|
|
10
|
+
type RegWrite = Callable[[Any, Model], tuple[int, ...]]
|
|
11
|
+
|
|
12
|
+
def compose(*funcs):
|
|
13
|
+
def composed_fn(arg: Any, *others):
|
|
14
|
+
for fn in funcs:
|
|
15
|
+
arg = fn(arg, *others)
|
|
16
|
+
return arg
|
|
17
|
+
return composed_fn
|
|
18
|
+
|
|
19
|
+
def read_flag(r: tuple[int, ...], _: Model):
|
|
20
|
+
return bool(r[0])
|
|
21
|
+
|
|
22
|
+
def read_int(r: tuple[int, ...], _: Model):
|
|
23
|
+
return r[0]
|
|
24
|
+
|
|
25
|
+
def write_int(v, _: Model) -> tuple[int, ...]:
|
|
26
|
+
return (int(v),)
|
|
27
|
+
|
|
28
|
+
def read_protection(r: tuple[int, ...], _: Model):
|
|
29
|
+
return Protection(r[0])
|
|
30
|
+
|
|
31
|
+
def read_mode(r: tuple[int, ...], _: Model):
|
|
32
|
+
return Mode(r[0])
|
|
33
|
+
|
|
34
|
+
def read_div(d, r: tuple[int, ...], _: Model):
|
|
35
|
+
return float(r[0] / d)
|
|
36
|
+
|
|
37
|
+
def div(d, x, _: Model):
|
|
38
|
+
return x / d
|
|
39
|
+
|
|
40
|
+
def read_dw(r: tuple[int, ...], _: Model):
|
|
41
|
+
return r[0] << 16 | r[1]
|
|
42
|
+
|
|
43
|
+
def read_temp(r: tuple[int, ...], _: Model):
|
|
44
|
+
return (-1 if r[0] else 1) * r[1]
|
|
45
|
+
|
|
46
|
+
def read_v(r: tuple[int, ...], m: Model):
|
|
47
|
+
return r[0] / m.v_scale
|
|
48
|
+
|
|
49
|
+
def write_v(v, m: Model, off = 0) -> tuple[int, ...]:
|
|
50
|
+
return (m.v_clamp(v, off),)
|
|
51
|
+
|
|
52
|
+
def read_i(r: tuple[int, ...], m: Model):
|
|
53
|
+
return r[0] / m.i_scale
|
|
54
|
+
|
|
55
|
+
def write_i(v, m: Model, off = 0) -> tuple[int, ...]:
|
|
56
|
+
return (m.i_clamp(v, off),)
|
|
57
|
+
|
|
58
|
+
class Field(Enum):
|
|
59
|
+
ID = 0, 1, read_int, None
|
|
60
|
+
SN = 1, 2, read_dw, None
|
|
61
|
+
FW = 3, 1, partial(read_div, 100), None
|
|
62
|
+
TEMP_C = 4, 2, read_temp, None
|
|
63
|
+
TEMP_F = 6, 2, read_temp, None
|
|
64
|
+
V_SET = 8, 1, read_v, write_v
|
|
65
|
+
I_SET = 9, 1, read_i, write_i
|
|
66
|
+
V = 10, 1, read_v, None
|
|
67
|
+
I = 11, 1, read_i, None
|
|
68
|
+
P = 12, 2, compose(read_dw, partial(div, 100)), None
|
|
69
|
+
V_IN = 14, 1, partial(read_div, 100), None
|
|
70
|
+
LOCK = 15, 1, read_flag, write_int
|
|
71
|
+
PROTECTION = 16, 1, read_protection, None
|
|
72
|
+
MODE = 17, 1, read_mode, None
|
|
73
|
+
EN = 18, 1, read_flag, write_int
|
|
74
|
+
PRESET = 19, 1, read_int, write_int
|
|
75
|
+
|
|
76
|
+
BAT_EN = 32, 1, read_flag, None
|
|
77
|
+
BAT_V = 33, 1, read_v, None
|
|
78
|
+
EXT_TEMP_C = 34, 2, read_temp, None
|
|
79
|
+
EXT_TEMP_F = 36, 2, read_temp, None
|
|
80
|
+
AH = 38, 2, compose(read_dw, partial(div, 1000)), None
|
|
81
|
+
WH = 40, 2, compose(read_dw, partial(div, 1000)), None
|
|
82
|
+
|
|
83
|
+
YEAR = 48, 1, read_int, write_int
|
|
84
|
+
MONTH = 49, 1, read_int, write_int
|
|
85
|
+
DAY = 50, 1, read_int, write_int
|
|
86
|
+
HOUR = 51, 1, read_int, write_int
|
|
87
|
+
MINUTE = 52, 1, read_int, write_int
|
|
88
|
+
SECOND = 53, 1, read_int, write_int
|
|
89
|
+
|
|
90
|
+
# Register 54 is a command register, not calibration data: writing 5377
|
|
91
|
+
# (0x1501) commits the calibration registers 55-62 to NVRAM. It is not
|
|
92
|
+
# readable because it carries no meaningful state.
|
|
93
|
+
CAL_COMMIT = 54, 1, None, write_int
|
|
94
|
+
CAL_V_ZERO = 55, 1, read_int, write_int
|
|
95
|
+
CAL_V_SCALE = 56, 1, read_int, write_int
|
|
96
|
+
CAL_BV_ZERO = 57, 1, read_int, write_int
|
|
97
|
+
CAL_BV_SCALE = 58, 1, read_int, write_int
|
|
98
|
+
CAL_I_ZERO = 59, 1, read_int, write_int
|
|
99
|
+
CAL_I_SCALE = 60, 1, read_int, write_int
|
|
100
|
+
CAL_BI_ZERO = 61, 1, read_int, write_int
|
|
101
|
+
CAL_BI_SCALE = 62, 1, read_int, write_int
|
|
102
|
+
|
|
103
|
+
TAKE_OK = 66, 1, read_flag, write_int
|
|
104
|
+
TAKE_OUT = 67, 1, read_flag, write_int
|
|
105
|
+
BOOT_EN = 68, 1, read_flag, write_int
|
|
106
|
+
VOLUME = 69, 1, read_flag, write_int
|
|
107
|
+
LOGO = 70, 1, read_flag, write_int
|
|
108
|
+
LANG = 71, 1, read_int, write_int
|
|
109
|
+
BACKLIGHT = 72, 1, read_int, write_int
|
|
110
|
+
|
|
111
|
+
M0_V = 80, 1, read_v, write_v
|
|
112
|
+
M0_A = 81, 1, read_i, write_i
|
|
113
|
+
M0_OVP = 82, 1, read_v, partial(write_v, off=2)
|
|
114
|
+
M0_OCP = 83, 1, read_i, write_i
|
|
115
|
+
|
|
116
|
+
M1_V = 84, 1, read_v, write_v
|
|
117
|
+
M1_A = 85, 1, read_i, write_i
|
|
118
|
+
M1_OVP = 86, 1, read_v, partial(write_v, off=2)
|
|
119
|
+
M1_OCP = 87, 1, read_i, write_i
|
|
120
|
+
|
|
121
|
+
M2_V = 88, 1, read_v, write_v
|
|
122
|
+
M2_A = 89, 1, read_i, write_i
|
|
123
|
+
M2_OVP = 90, 1, read_v, partial(write_v, off=2)
|
|
124
|
+
M2_OCP = 91, 1, read_i, write_i
|
|
125
|
+
|
|
126
|
+
M3_V = 92, 1, read_v, write_v
|
|
127
|
+
M3_A = 93, 1, read_i, write_i
|
|
128
|
+
M3_OVP = 94, 1, read_v, partial(write_v, off=2)
|
|
129
|
+
M3_OCP = 95, 1, read_i, write_i
|
|
130
|
+
|
|
131
|
+
M4_V = 96, 1, read_v, write_v
|
|
132
|
+
M4_A = 97, 1, read_i, write_i
|
|
133
|
+
M4_OVP = 98, 1, read_v, partial(write_v, off=2)
|
|
134
|
+
M4_OCP = 99, 1, read_i, write_i
|
|
135
|
+
|
|
136
|
+
M5_V = 100, 1, read_v, write_v
|
|
137
|
+
M5_A = 101, 1, read_i, write_i
|
|
138
|
+
M5_OVP = 102, 1, read_v, partial(write_v, off=2)
|
|
139
|
+
M5_OCP = 103, 1, read_i, write_i
|
|
140
|
+
|
|
141
|
+
M6_V = 104, 1, read_v, write_v
|
|
142
|
+
M6_A = 105, 1, read_i, write_i
|
|
143
|
+
M6_OVP = 106, 1, read_v, partial(write_v, off=2)
|
|
144
|
+
M6_OCP = 107, 1, read_i, write_i
|
|
145
|
+
|
|
146
|
+
M7_V = 108, 1, read_v, write_v
|
|
147
|
+
M7_A = 109, 1, read_i, write_i
|
|
148
|
+
M7_OVP = 110, 1, read_v, partial(write_v, off=2)
|
|
149
|
+
M7_OCP = 111, 1, read_i, write_i
|
|
150
|
+
|
|
151
|
+
M8_V = 112, 1, read_v, write_v
|
|
152
|
+
M8_A = 113, 1, read_i, write_i
|
|
153
|
+
M8_OVP = 114, 1, read_v, partial(write_v, off=2)
|
|
154
|
+
M8_OCP = 115, 1, read_i, write_i
|
|
155
|
+
|
|
156
|
+
M9_V = 116, 1, read_v, write_v
|
|
157
|
+
M9_A = 117, 1, read_i, write_i
|
|
158
|
+
M9_OVP = 118, 1, read_v, partial(write_v, off=2)
|
|
159
|
+
M9_OCP = 119, 1, read_i, write_i
|
|
160
|
+
|
|
161
|
+
addr: int
|
|
162
|
+
count: int
|
|
163
|
+
read: RegRead | None
|
|
164
|
+
write: RegWrite | None
|
|
165
|
+
|
|
166
|
+
def __new__(cls, addr: int, count: int, read: RegRead | None, write: RegWrite | None):
|
|
167
|
+
obj = object.__new__(cls)
|
|
168
|
+
obj._value_ = addr
|
|
169
|
+
obj.addr = addr
|
|
170
|
+
obj.count = count
|
|
171
|
+
obj.read = read
|
|
172
|
+
obj.write = write
|
|
173
|
+
return obj
|
|
174
|
+
|
|
175
|
+
@override
|
|
176
|
+
@classmethod
|
|
177
|
+
def _missing_(cls, value: object) -> Field | None:
|
|
178
|
+
if isinstance(value, str):
|
|
179
|
+
return cls.__members__.get(value.upper())
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def label(self) -> str:
|
|
184
|
+
return self.name.lower()
|
|
185
|
+
|
|
186
|
+
@override
|
|
187
|
+
def __str__(self) -> str:
|
|
188
|
+
return self.label
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def is_readable(self):
|
|
192
|
+
return self.read is not None
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def is_writable(self):
|
|
196
|
+
return self.write is not None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def flatten_fields(fields: Sequence[str | Field | Sequence[str | Field]]):
|
|
200
|
+
for field in fields:
|
|
201
|
+
match field:
|
|
202
|
+
case Field(): yield field
|
|
203
|
+
case str(): yield Field(field)
|
|
204
|
+
case _: yield from flatten_fields(field)
|
riden/model.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Model(Enum):
|
|
6
|
+
RD6006 = (range(60060, 60065), 100, 1000, 60, 6)
|
|
7
|
+
RD6006P = ([60065], 1000, 10000, 60, 6)
|
|
8
|
+
RK6006 = ([60066], 100, 1000, 60, 6)
|
|
9
|
+
RD6012 = (range(60120, 60125), 100, 100, 60, 12)
|
|
10
|
+
RD6012P = (range(60125, 60130), 1000, 100, 60, 12)
|
|
11
|
+
RD6018 = (range(60180, 60190), 100, 100, 60, 18)
|
|
12
|
+
RD6024 = (range(60241, 60250), 100, 100, 60, 24)
|
|
13
|
+
|
|
14
|
+
def __init__(self, codes: Sequence[int], v_scale: int, i_scale: int, v_max: int, i_max: int):
|
|
15
|
+
self.codes = codes
|
|
16
|
+
self.v_scale = v_scale
|
|
17
|
+
self.i_scale = i_scale
|
|
18
|
+
self.v_max = v_max
|
|
19
|
+
self.i_max = i_max
|
|
20
|
+
|
|
21
|
+
def v_clamp(self, value: float, offset: float = 0) -> int:
|
|
22
|
+
return round(max(0, min(self.v_max + offset, value)) * self.v_scale)
|
|
23
|
+
|
|
24
|
+
def i_clamp(self, value: float, offset: float = 0) -> int:
|
|
25
|
+
return round(max(0, min(self.i_max + offset, value)) * self.i_scale)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def find(code: int) -> Model:
|
|
29
|
+
for model in Model:
|
|
30
|
+
if code in model.codes:
|
|
31
|
+
return model
|
|
32
|
+
return next(iter(Model))
|
riden/py.typed
ADDED
|
File without changes
|