rgscore 0.14.2__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.
rgscore/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .model.register import EnumValue, Field, FieldDef, Register, RLink, RLinkI2C
2
+ from .model.reglist import RegList
3
+ from .model.sfix import S, U
File without changes
@@ -0,0 +1,584 @@
1
+ import dataclasses
2
+ import json
3
+ import re
4
+ from abc import ABC, abstractmethod
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from functools import reduce
8
+ from typing import Any, Self, final
9
+
10
+ from bitstring import BitArray
11
+ from i2c_api import I2CMaster, RegisterAddress
12
+ from pydantic import TypeAdapter
13
+
14
+
15
+ class RLink(ABC):
16
+ @abstractmethod
17
+ def read(self, addr: int, address_bus_width: int, value_width: int) -> BitArray | None:
18
+ """
19
+ Reads raw register value (of `value_width` bits) from address `addr` assuming that
20
+ address bus is `address_bus_width`. If no register exist at this address, then returns None.
21
+ """
22
+
23
+ @abstractmethod
24
+ def write(self, addr: int, address_bus_width: int, value: BitArray) -> bool:
25
+ """
26
+ Writes register value `value` to address `addr` assuming that address bus is `address_bus_width`.
27
+ If register does exist at this address, then do write and return True, otherwise return False.
28
+ """
29
+
30
+
31
+ @final
32
+ class RLinkI2C(RLink):
33
+ def __init__(self, i2c: I2CMaster, device_address: int):
34
+ self.i2c = i2c
35
+ self.device_address = device_address
36
+
37
+ def read(self, addr: int, address_bus_width: int, width: int) -> BitArray | None:
38
+ bytes_to_read = int(width / 8)
39
+ if (width % 8) > 0:
40
+ bytes_to_read += 1
41
+
42
+ data = self.i2c.read_register(
43
+ address=self.device_address,
44
+ register=RegisterAddress(addr, address_bus_width),
45
+ num_bytes=bytes_to_read,
46
+ )
47
+
48
+ if data is None:
49
+ return None
50
+ else:
51
+ return BitArray(data[0:width])
52
+
53
+ def write(self, addr: int, address_bus_width: int, value: BitArray) -> bool:
54
+ return self.i2c.write_register(
55
+ address=self.device_address,
56
+ register=RegisterAddress(addr, address_bus_width),
57
+ data=value,
58
+ )
59
+
60
+
61
+ @dataclass
62
+ class EnumValue:
63
+ name: str
64
+ value: float
65
+ desc: str = ""
66
+
67
+ def __post_init__(self):
68
+ self.value = float(self.value)
69
+
70
+ def __repr__(self) -> str:
71
+ return f"{self.value} = {self.name} # {self.desc}"
72
+
73
+
74
+ @dataclass
75
+ class FieldDef:
76
+ name: str
77
+ offset: int
78
+ signed: str
79
+ width: int
80
+ fractional: int
81
+ rw: bool
82
+ desc: str = ""
83
+ enum_values: list[EnumValue] | None = None
84
+
85
+ def __post_init__(self):
86
+ self.__validate__()
87
+
88
+ def __validate__(self):
89
+ if self.signed not in ("U", "S"):
90
+ raise ValueError("Invalid signed value")
91
+ if self.offset < 0:
92
+ raise ValueError("Field offset must be a positive integer")
93
+
94
+ if self.enum_values is not None:
95
+ vlen = len(self.enum_values)
96
+ if len({v.name for v in self.enum_values}) < vlen or len({v.value for v in self.enum_values}) < vlen:
97
+ raise ValueError("List of valid values contains duplicate entries")
98
+ else:
99
+ self.valid_name_values = {v.name: v for v in self.enum_values}
100
+ self.valid_value_values = {v.value: v for v in self.enum_values}
101
+ else:
102
+ self.valid_name_values = {}
103
+ self.valid_value_values = {}
104
+
105
+ def copy(self) -> Self:
106
+ return dataclasses.replace(self)
107
+
108
+ def range(self) -> tuple[float, float]:
109
+ if self.signed == "U":
110
+ return (0.0, (pow(2, self.width) - 1) / pow(2, self.fractional))
111
+ else:
112
+ return (
113
+ -(pow(2, self.width - 1) / pow(2, self.fractional)),
114
+ (pow(2, self.width - 1) - 1) / pow(2, self.fractional),
115
+ )
116
+
117
+ def end_offset(self) -> int:
118
+ return self.offset + self.width - 1
119
+
120
+ def idxs(self) -> list[int]:
121
+ return list(range(self.offset, self.end_offset() + 1))
122
+
123
+ def format(self, value: float) -> str:
124
+ return f"{int(value)}" if self.fractional == 0 else f"{value}"
125
+
126
+ def __repr__(self):
127
+ rw = "rw" if self.rw else "ro"
128
+ return f"{self.name}@[{self.end_offset()}:{self.offset}]{self.signed}{self.width}.{self.fractional}#{rw}"
129
+
130
+ def read_raw(self, data: BitArray) -> BitArray:
131
+ return data[(len(data) - self.end_offset() - 1) : (len(data) - self.offset)]
132
+
133
+ def next_up_value(self, data: BitArray) -> float | int:
134
+ return self.__next(data, 1)
135
+
136
+ def next_down_value(self, data: BitArray) -> float | int:
137
+ return self.__next(data, -1)
138
+
139
+ def __get_next_raw_field(self, data: BitArray, step: int) -> BitArray:
140
+ raw_field = self.read_raw(data)
141
+ try:
142
+ if self.signed == "S":
143
+ next_in_val = raw_field.int + step
144
+ return BitArray(f"int:{len(raw_field)}={next_in_val}")
145
+ else: # unsigned
146
+ next_in_val = raw_field.uint + step
147
+ return BitArray(f"uint:{len(raw_field)}={next_in_val}")
148
+ except ValueError:
149
+ # we are here if next value is out of range for this field
150
+ return raw_field
151
+
152
+ def __next(self, data: BitArray, step: int) -> float | int:
153
+ return self.__read_from_raw_field(self.__get_next_raw_field(data, step))
154
+
155
+ def __read_from_raw_field(self, raw_field: BitArray) -> float | int:
156
+ if self.fractional == 0:
157
+ return raw_field.int if self.signed == "S" else raw_field.uint
158
+ else:
159
+ return (raw_field.int if self.signed == "S" else raw_field.uint) / pow(2, self.fractional)
160
+
161
+ def read(self, data: BitArray) -> float | int:
162
+ return self.__read_from_raw_field(self.read_raw(data))
163
+
164
+ def write(self, data: BitArray, value: float) -> None:
165
+ new_val = int(value * pow(2, self.fractional))
166
+ tspec = ("int:" if self.signed == "S" else "uint:") + f"{self.width}={new_val}"
167
+ data.overwrite(len(data) - self.end_offset() - 1, BitArray(tspec))
168
+
169
+ @staticmethod
170
+ def value_of(field_definition: str) -> "FieldDef":
171
+ m = Register.field_def_re.match(field_definition)
172
+ if m:
173
+ end_offset = int(m.group("end"))
174
+
175
+ start_group = m.group("start")
176
+ start_offset = int(start_group) if start_group else end_offset
177
+
178
+ implied_width = end_offset - start_offset + 1
179
+ width = int(m.group("w"))
180
+ if implied_width != width:
181
+ raise ValueError(f"Inconsistent field width {implied_width} != {width}")
182
+ rw = m.group("rw")
183
+ rw = rw if rw else "rw"
184
+ return FieldDef(
185
+ name=m.group("name"),
186
+ offset=start_offset,
187
+ signed=m.group("s"),
188
+ width=width,
189
+ fractional=int(m.group("f")),
190
+ rw=(rw == "rw"),
191
+ )
192
+ else:
193
+ raise ValueError(f"Invalid register field definition [{field_definition}]")
194
+
195
+ def with_description(self, desc: str) -> Self:
196
+ self.desc = desc
197
+ return self
198
+
199
+ def with_enum(self, values: list[EnumValue]) -> Self:
200
+ if len({v.name for v in values}) != len(values):
201
+ raise ValueError("Duplicate name in enum values")
202
+ if len({v.value for v in values}) != len(values):
203
+ raise ValueError("Duplicate value in enum values")
204
+ self.enum_values = values
205
+ self.__validate__()
206
+ return self
207
+
208
+
209
+ class Register:
210
+ numeric_type_re = re.compile(r"^\s*(?P<s>U|S)(?P<w>\d+)\.(?P<f>\d+)\s*$")
211
+ rw_re = re.compile(r"^\s*(?P<rw>rw|ro)\s*$")
212
+ field_def_re = re.compile(
213
+ r"^(?P<name>[a-zA-Z_]+[0-9]*)@\[(?P<end>\d+)(:(?P<start>\d+))?\](?P<s>U|S)(?P<w>\d+)\.(?P<f>\d+)(#(?P<rw>rw|ro))?$"
214
+ )
215
+ register_name_re = re.compile("^[a-zA-Z]+[a-zA-Z0-9_]*$")
216
+
217
+ def __init__(
218
+ self,
219
+ bit_len: int,
220
+ model: list[FieldDef] | None = None,
221
+ address: int | None = None,
222
+ address_bus_width_bytes: int = 1,
223
+ name: str | None = None,
224
+ link_provider: Callable[[], RLink] | None = None,
225
+ desc: str = "",
226
+ ):
227
+ """
228
+ Register in an object that encapsulates array of bits and field definitions and provides read and write
229
+ access to these fields. Fields are defined in the model
230
+
231
+ Args:
232
+ bit_len: width of register in bits
233
+ model: model of register fields
234
+ address: register address (optional)
235
+ address_bus_width_bytes: width of address bus in bytes
236
+ name: register name (optional)
237
+ link_provider: provider of link to the external storage (optional)
238
+ desc: register description (optional)
239
+ """
240
+ if name is not None and not Register.register_name_re.match(name):
241
+ raise ValueError("Invalid register name.")
242
+
243
+ if bit_len <= 0:
244
+ raise ValueError("Register width must be greater than zero")
245
+ self.data: BitArray = BitArray.from_zeros(bit_len)
246
+ self._originial_data = BitArray.from_zeros(bit_len)
247
+ self.__width = bit_len
248
+ self.__desc = desc
249
+
250
+ self._model = (
251
+ [FieldDef.value_of(f"val@[{bit_len - 1}:0]U{bit_len}.0")] if model is None else [m.copy() for m in model]
252
+ )
253
+ self._model.sort(key=lambda fd: -fd.offset)
254
+
255
+ if self._model == []:
256
+ raise ValueError(
257
+ "Register model must not be empty list. Leaving it None will create an implicit field though."
258
+ )
259
+ self._fields_by_name: dict[str, FieldDef] = {fd.name: fd for fd in self._model}
260
+ if len(self._fields_by_name) != len(self._model):
261
+ raise ValueError("Duplicate field names in the model")
262
+ self.__validate_model_wrt_width(self._model, self.width)
263
+ self.__validate_fields_overlapp(self._model)
264
+
265
+ self.address = address
266
+ self.address_bus_width_bytes = address_bus_width_bytes
267
+ self.name = name
268
+ self.__link_provider: Callable[[], RLink] | None = link_provider
269
+ self.linked_address: int | None = address
270
+
271
+ def __validate_model_wrt_width(self, model: list[FieldDef], width: int):
272
+ for m in model:
273
+ if m.end_offset() >= width or m.offset < 0:
274
+ raise ValueError(f'Field "{m.name}" extends outside of register width.')
275
+
276
+ def __validate_fields_overlapp(self, model: list[FieldDef]):
277
+ fields_idxs = reduce(lambda x, y: x + y, [m.idxs() for m in model], [])
278
+ if len(set(fields_idxs)) != len(fields_idxs):
279
+ raise ValueError("Overlapping fields in register definition.")
280
+
281
+ @property
282
+ def desc(self):
283
+ return self.__desc
284
+
285
+ @desc.setter
286
+ def desc(self, desc: str):
287
+ self.__desc = desc
288
+
289
+ @property
290
+ def width(self) -> int:
291
+ return self.__width
292
+
293
+ @width.setter
294
+ def width(self, new_width: int):
295
+ self.__validate_model_wrt_width(self._model, new_width)
296
+ self.__width = new_width
297
+ self._originial_data = BitArray.from_zeros(new_width)
298
+ self.data = BitArray.from_zeros(new_width)
299
+
300
+ def replace_model(self, new_model: list[FieldDef]):
301
+ field_names: set[str] = {fd.name for fd in new_model}
302
+ if len(field_names) != len(new_model):
303
+ raise ValueError("Duplicate field names in the model")
304
+ self.__validate_fields_overlapp(new_model)
305
+ self.__validate_model_wrt_width(new_model, self.width)
306
+ self._model.clear()
307
+ self._model.extend(new_model)
308
+ self._model.sort(key=lambda fd: -fd.offset)
309
+
310
+ def copy(self) -> "Register":
311
+ register_copy = Register(
312
+ bit_len=self.width,
313
+ model=[fd.copy() for fd in self._model],
314
+ address=self.address,
315
+ name=self.name,
316
+ link_provider=self.__link_provider,
317
+ )
318
+ register_copy.data = self.data.copy()
319
+ register_copy._originial_data = self._originial_data.copy()
320
+ return register_copy
321
+
322
+ def is_changed(self) -> bool:
323
+ """
324
+ Returns True or False indicating of register data has changed since it was loaded. This method is only usefull
325
+ when external storage is linked to the register and read() and write() methods are called.
326
+ """
327
+ return self._originial_data != self.data
328
+
329
+ def get_def(self) -> str:
330
+ """Returns human-readable register definition."""
331
+ return (
332
+ "Register"
333
+ + ("" if self.name is None else f" name:{self.name}")
334
+ + ("" if self.address is None else f" addr:0x{self.address:X}")
335
+ + ("" if self.desc == "" else f' desc:"{self.desc}"')
336
+ + f" width:{self.width} fields:{self._model}"
337
+ )
338
+
339
+ def __repr__(self):
340
+ retval = "Register" if self.name is None else self.name
341
+ if self.address is not None:
342
+ retval += f"[0x{self.address:0{self.address_bus_width_bytes * 2}X}]"
343
+ return (
344
+ f"{retval}("
345
+ + ", ".join(
346
+ [f"{field_def.name}={field_def.format(field_def.read(self.data))}" for field_def in self._model]
347
+ )
348
+ + ")"
349
+ )
350
+
351
+ def to_dict_def(self) -> dict:
352
+ """Exports register definition as dict."""
353
+ return {
354
+ "name": self.name,
355
+ "address": None if self.address is None else f"0x{self.address:X}",
356
+ "address_bus_width_bytes": self.address_bus_width_bytes,
357
+ "width": self.width,
358
+ "desc": self.desc,
359
+ "fields": [dataclasses.asdict(field_def) for field_def in self._model],
360
+ }
361
+
362
+ def to_json_def(self, indent: int | None = None) -> str:
363
+ """Exports register definition as json text."""
364
+ return json.dumps(self.to_dict_def(), indent=indent)
365
+
366
+ @staticmethod
367
+ def from_dict_def(data: dict) -> "Register":
368
+ fd_adapter = TypeAdapter(FieldDef)
369
+ return Register(
370
+ bit_len=data["width"],
371
+ name=data["name"],
372
+ desc=data["desc"] if "desc" in data else "",
373
+ address=None if data["address"] is None else int(data["address"], 16),
374
+ address_bus_width_bytes=data["address_bus_width_bytes"],
375
+ model=[fd_adapter.validate_python(f) for f in data["fields"]],
376
+ )
377
+
378
+ @staticmethod
379
+ def from_json_def(json_str: str) -> "Register":
380
+ """Imports register definition from json text and returns instance of Register."""
381
+ return Register.from_dict_def(json.loads(json_str))
382
+
383
+ def set_field_value(self, field: str, value: float | str) -> float | int:
384
+ """
385
+ Sets value to a field or raises exception if unable to do so. If value is str, then it is used as a look up
386
+ into associated enum if any and found number value is used. If data in the register model in memory actually
387
+ changed as a result of calling this method, then calling `Register::is_changed()` starts returning True.
388
+ """
389
+ field_def = self._fields_by_name.get(field)
390
+ if field_def is None:
391
+ raise ValueError(f'There is no field "{field}" in this register.')
392
+ if not field_def.rw:
393
+ raise ValueError(f'Field "{field}" is not writable.')
394
+
395
+ def get_value() -> float:
396
+ if isinstance(value, str):
397
+ num_val = field_def.valid_name_values.get(value)
398
+ if num_val is None:
399
+ raise ValueError(f"Invalid enum value [{value}] for field {field}")
400
+ else:
401
+ return num_val.value
402
+ else:
403
+ return value
404
+
405
+ value = get_value()
406
+ min, max = field_def.range()
407
+ if min <= value <= max:
408
+ field_def.write(self.data, value)
409
+ return field_def.read(self.data)
410
+ else:
411
+ raise ValueError(f'Value {value} is out of range [{field_def.range()}] for field "{field}"')
412
+
413
+ def get_field_value(self, field: str) -> float | int:
414
+ field_def = self._fields_by_name.get(field)
415
+ if field_def is None:
416
+ raise ValueError(f'There is no field "{field}" in this register.')
417
+ else:
418
+ return field_def.read(self.data)
419
+
420
+ def get_next_up_field_value(self, field: str) -> float | int:
421
+ field_def = self._fields_by_name.get(field)
422
+ if field_def is None:
423
+ raise ValueError(f'There is no field "{field}" in this register.')
424
+ else:
425
+ return field_def.next_up_value(self.data)
426
+
427
+ def get_next_down_field_value(self, field: str) -> float | int:
428
+ field_def = self._fields_by_name.get(field)
429
+ if field_def is None:
430
+ raise ValueError(f'There is no field "{field}" in this register.')
431
+ else:
432
+ return field_def.next_down_value(self.data)
433
+
434
+ def get_field_value_raw(self, field: str) -> BitArray:
435
+ field_def = self._fields_by_name.get(field)
436
+ if field_def is None:
437
+ raise ValueError(f'There is no field "{field}" in this register.')
438
+ else:
439
+ return field_def.read_raw(self.data)
440
+
441
+ def get_field_values(self) -> dict[str, float]:
442
+ return {m.name: self.get_field_value(m.name) for m in self._model}
443
+
444
+ def get_field_definition(self, field: str) -> FieldDef:
445
+ return self._fields_by_name[field]
446
+
447
+ def get_field_names(self) -> list[str]:
448
+ return [m.name for m in self._model]
449
+
450
+ def link(self, link: RLink, register_address: int | None = None) -> None:
451
+ """
452
+ Links register to external store. If optional address is provided, then it will be used to access external store
453
+ even if register has address in its definition. If optional address is not provided, then address defined in the
454
+ register will be used. If no addresses are given and present in the internal register definition then ValueError
455
+ will be raised.
456
+
457
+ :param link: link to external store
458
+ :param register_address: address of register in external store
459
+ """
460
+ if register_address is not None:
461
+ self.linked_address = register_address
462
+ if self.linked_address is None:
463
+ raise ValueError(
464
+ "To link register to external store address must be either set in the register "
465
+ "or provided during linking."
466
+ )
467
+ self.__link_provider = lambda: link
468
+
469
+ def set_link_provider(self, link_provider: Callable[[], RLink]) -> None:
470
+ self.__link_provider = link_provider
471
+
472
+ def read(self) -> None:
473
+ """
474
+ Reads register data from associated link (external store). If no link is configured, then this is a no-op.
475
+ If external store store (link) is configure and read operation fails for any reason than RuntimeError is raised.
476
+ Once data is read from the external store, calling `Register::is_changed()` starts returning False until
477
+ data is modified by calling `Register::set_field_value(...)`.
478
+ """
479
+ if self.__link_provider is not None:
480
+ assert self.linked_address is not None
481
+ raw_data = self.__link_provider().read(self.linked_address, self.address_bus_width_bytes, self.width)
482
+ if raw_data is None:
483
+ raise RuntimeError(f"There is no register at the address {self.linked_address}.")
484
+ self.data = raw_data
485
+ self._originial_data = raw_data.copy()
486
+
487
+ def write(self, read_back: bool = False, if_changed_only: bool = False) -> bool:
488
+ """
489
+ Writes register data to external store configured when `Register::link(...)` method is called.
490
+ Once data is written to the external store, calling `Register::is_changed()` starts returning False until
491
+ data is modified by calling `Register::set_field_value(...)`.
492
+
493
+ :param read_back: If True, then immediately after write operation read it back. If False, then do just write op.
494
+ :param if_changed_only: If True, then check if register in memory model changed and if so then do write,
495
+ otherwise do not write (return False). If False (default), then always write.
496
+ :return: True or False if write operation was successful or not.
497
+ """
498
+ if self.__link_provider is None or (if_changed_only and self._originial_data == self.data):
499
+ return False
500
+ else:
501
+ assert self.linked_address is not None
502
+ write_success = self.__link_provider().write(self.linked_address, self.address_bus_width_bytes, self.data)
503
+ if read_back:
504
+ self.read()
505
+ if write_success:
506
+ self._originial_data = self.data.copy()
507
+ return write_success
508
+
509
+ def mk_embedding_class(self, link_provider: Callable[[], RLink], auto_sync: bool) -> Any:
510
+ register = self.copy()
511
+ register.set_link_provider(link_provider)
512
+
513
+ attrs = {
514
+ "_read": register.read,
515
+ "_write": register.write,
516
+ "__repr__": register.__repr__,
517
+ "__call__": lambda _: register.data,
518
+ }
519
+ for field_name in self.get_field_names():
520
+
521
+ def mk_accessor(fname, register, auto_sync):
522
+ def field_accessor(sself, new_value=None) -> int | float:
523
+ if new_value is None:
524
+ if auto_sync:
525
+ register.read()
526
+ return register.get_field_value(fname)
527
+ else:
528
+ retval = register.set_field_value(fname, new_value)
529
+ if auto_sync:
530
+ register.write()
531
+ return retval
532
+
533
+ return field_accessor
534
+
535
+ def mk_raw_accessor(fname, register):
536
+ def field_accessor(sself) -> BitArray:
537
+ if auto_sync:
538
+ register.read()
539
+ return register.get_field_value_raw(fname)
540
+
541
+ return field_accessor
542
+
543
+ accessor = mk_accessor(field_name, register, auto_sync)
544
+ attrs[field_name] = property(fget=accessor, fset=accessor)
545
+ attrs[field_name + "_raw"] = property(fget=mk_raw_accessor(field_name, register))
546
+
547
+ return type(self.name, (), attrs)
548
+
549
+
550
+ def Field(
551
+ name: str, offset: int, numeric_type: str, rw: str = "rw", desc: str = "", values: list[EnumValue] | None = None
552
+ ) -> FieldDef:
553
+ """
554
+ Convinience method for creating instance of `FieldDef`.
555
+
556
+ Args:
557
+ name: field name
558
+ offset: field offset from the right
559
+ numeric_type: fixed point number type, e.g. U5.7 or S3.0 etc.
560
+ rw: "rw" (default) or "ro" - read/write or read/only
561
+ desc: optiona field description
562
+ values: optional enum values
563
+
564
+ Returns:
565
+ Instance of `FieldDef`.
566
+ """
567
+ nt_m = Register.numeric_type_re.match(numeric_type)
568
+ if nt_m is None:
569
+ raise ValueError(f"Invalid numeric type: {numeric_type}")
570
+
571
+ rw_m = Register.rw_re.match(rw)
572
+ if rw_m is None:
573
+ raise ValueError(f"Invalid rw: {rw}")
574
+
575
+ return FieldDef(
576
+ name=name,
577
+ offset=offset,
578
+ width=int(nt_m.group("w")),
579
+ signed=nt_m.group("s"),
580
+ fractional=int(nt_m.group("f")),
581
+ rw=(rw == "rw"),
582
+ desc=desc,
583
+ enum_values=values,
584
+ )
@@ -0,0 +1,154 @@
1
+ import json
2
+ from collections.abc import Callable
3
+ from typing import Self
4
+
5
+ from rgscore.model.register import Register, RLink
6
+
7
+
8
+ class RegList:
9
+ def __init__(
10
+ self,
11
+ store: RLink | None = None,
12
+ on_change_callback: Callable[[Self], None] = lambda _: None,
13
+ registers: list[Register] | None = None,
14
+ ):
15
+ super().__init__()
16
+ self.registers: list[Register] = []
17
+ self._reg_names: set[str] = set()
18
+ self._reg_addresses: set[int] = set()
19
+ self._reg_by_names: dict[str, Register] = {}
20
+ self._reg_by_address: dict[int, Register] = {}
21
+ self._store = store
22
+ self._on_change_callback = on_change_callback
23
+
24
+ if registers is not None:
25
+ self.add(registers)
26
+
27
+ def add(self, rs: Register | list[Register]):
28
+ self.__add(rs, call_on_change_callback=True)
29
+
30
+ def __add(self, rs: Register | list[Register], call_on_change_callback: bool):
31
+ rs = [rs] if isinstance(rs, Register) else rs
32
+ if len(rs) == 0:
33
+ return
34
+
35
+ for r in rs:
36
+ if r.address is None:
37
+ raise ValueError("To be added Register must have an address")
38
+
39
+ if r.name is None:
40
+ # create implicit register name if register does not an explicit one
41
+ r.name = f"R{r.address}"
42
+
43
+ if r.name in self._reg_names:
44
+ raise ValueError(f"Register under a name [{r.name}] is already in the set")
45
+
46
+ if r.address in self._reg_addresses:
47
+ raise ValueError(f"Register at this address [0x{r.address:04X}] is already in the set")
48
+
49
+ if self._store is not None:
50
+ r.link(self._store)
51
+
52
+ self.registers.append(r)
53
+ self._reg_names.add(r.name)
54
+ self._reg_addresses.add(r.address)
55
+ self._reg_by_names[r.name] = r
56
+ self._reg_by_address[r.address] = r
57
+
58
+ self.registers.sort(key=lambda r: r.address)
59
+ if call_on_change_callback:
60
+ self._on_change_callback(self)
61
+
62
+ def get_register_by_name(self, name: str) -> Register | None:
63
+ return self._reg_by_names.get(name)
64
+
65
+ def get_register_by_address(self, address: int) -> Register | None:
66
+ return self._reg_by_address.get(address)
67
+
68
+ def delete_register_by_name(self, name: str):
69
+ self.__delete_register_by_name(name, call_on_change_callback=True)
70
+
71
+ def __delete_register_by_name(self, name: str, call_on_change_callback: bool):
72
+ """
73
+ Removes register with a given name from this regList. If register with this name not found, then do nothing.
74
+ """
75
+ if name in self._reg_names:
76
+ register = self._reg_by_names[name]
77
+ self._reg_names.remove(name)
78
+
79
+ assert register.address is not None
80
+ self._reg_addresses.remove(register.address)
81
+ del self._reg_by_names[name]
82
+ del self._reg_by_address[register.address]
83
+
84
+ idx = [r.name for r in self.registers].index(name)
85
+ del self.registers[idx]
86
+ if call_on_change_callback:
87
+ self._on_change_callback(self)
88
+
89
+ def update_register_def(self, original_register: Register, new_register: Register) -> None:
90
+ """
91
+ Updates register in the regList given the original one and a new one which is expected to replace original.
92
+ """
93
+ if new_register.address is None:
94
+ raise ValueError("To be added Register must have an address")
95
+
96
+ if new_register.name is None:
97
+ raise ValueError("To be added Register must have a name")
98
+
99
+ if new_register.name != original_register.name and new_register.name in self._reg_names:
100
+ raise ValueError(f"Register under a name [{new_register.name}] is already in the set")
101
+
102
+ if new_register.address != original_register.address and new_register.address in self._reg_addresses:
103
+ raise ValueError(f"Register at this address [{new_register.address}] is already in the set")
104
+
105
+ assert original_register.name is not None
106
+ self.__delete_register_by_name(original_register.name, call_on_change_callback=False)
107
+ self.__add(new_register, call_on_change_callback=False)
108
+ self._on_change_callback(self)
109
+
110
+ def clear(self):
111
+ """Removes all registers from the list."""
112
+ self.registers.clear()
113
+ self._reg_names.clear()
114
+ self._reg_addresses.clear()
115
+ self._reg_by_names.clear()
116
+ self._reg_by_address.clear()
117
+ self._on_change_callback(self)
118
+
119
+ def read_all(self) -> None:
120
+ """Reads all registers in the regList from linked store if any provided."""
121
+ for r in self.registers:
122
+ r.read()
123
+
124
+ def write_all(self) -> None:
125
+ """Writes all registers in the regList to linked store if any provided."""
126
+ for r in self.registers:
127
+ r.write()
128
+
129
+ def to_json_def(self, indent: int | None = None) -> str:
130
+ """Exports register definition contained within regList as json text."""
131
+ return json.dumps(
132
+ {
133
+ "class": "RegList",
134
+ "version": 1,
135
+ "registers": [r.to_dict_def() for r in self.registers],
136
+ },
137
+ indent=indent,
138
+ )
139
+
140
+ @staticmethod
141
+ def from_json_def(json_str: str) -> "RegList":
142
+ """Imports a RegList definition from a json string returning new instance of RegList."""
143
+ return RegList().load_from_json_def(json_str)
144
+
145
+ def load_from_json_def(self, json_str: str) -> Self:
146
+ data = json.loads(json_str)
147
+ if data["class"] != "RegList":
148
+ raise ValueError("Provided json document does not represent regList")
149
+ elif data["version"] != 1:
150
+ raise ValueError(f"Provided regList json has unsupported version {data['version']}")
151
+ else:
152
+ self.clear()
153
+ self.add([Register.from_dict_def(d) for d in data["registers"]])
154
+ return self
rgscore/model/sfix.py ADDED
@@ -0,0 +1,26 @@
1
+ from functools import cache, reduce
2
+
3
+ from bitstring import BitArray, Bits
4
+
5
+
6
+ @cache
7
+ def __2pow(n) -> float | int:
8
+ return pow(2, n)
9
+
10
+
11
+ def __to_bit_array(data: int | Bits) -> Bits:
12
+ return BitArray(f"uint:8={data}") if isinstance(data, int) else data
13
+
14
+
15
+ def U(en: int, bit_arrays: list[Bits | int]) -> float | int:
16
+ return reduce(
17
+ lambda acc, v: acc + v, [__to_bit_array(a) for a in bit_arrays], BitArray.from_zeros(0)
18
+ ).uint / __2pow(en)
19
+
20
+
21
+ def S(en: int, bit_arrays: list[Bits | int]) -> float | int:
22
+ return reduce(
23
+ lambda acc, v: acc + v,
24
+ [__to_bit_array(a) for a in bit_arrays],
25
+ BitArray.from_zeros(0),
26
+ ).int / __2pow(en)
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: rgscore
3
+ Version: 0.14.2
4
+ Summary: Python API for describing and accessing hardware registers
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: bitstring<6.0.0,>=5.0.0
9
+ Requires-Dist: i2c-api>=0.20.0
10
+ Requires-Dist: pydantic>=2.14.1
11
+ Dynamic: license-file
12
+
13
+ # RGSCore
14
+
15
+ [![License - MIT](https://img.shields.io/badge/License-MIT-blue)](https://github.com/Brainless-Software/rgscore/blob/master/LICENSE)
16
+ [![PyPI Latest Release](https://img.shields.io/pypi/v/rgscore.svg)](https://pypi.org/project/rgscore/)
17
+ [![pytest](https://github.com/Brainless-Software/rgscore/actions/workflows/test.yml/badge.svg)](https://github.com/Brainless-Software/rgscore/actions/workflows/test.yml)
18
+ [![Documentation](https://img.shields.io/badge/docs-latest-brightgreen)](https://brainless-software.github.io/rgscore/)
19
+
20
+ Python library that provides classes for representing, defining and accessing registers in the embedded devices.
21
+
22
+ Example:
23
+
24
+ ```python
25
+ from rgscore import Register, RegList, FieldDef
26
+
27
+ reglist = RegList(
28
+ registers=[
29
+ Register(
30
+ bit_len=8,
31
+ name="Foo",
32
+ address=0x01,
33
+ model=[FieldDef.value_of("a@[3:0]U4.1#ro")],
34
+ ),
35
+ Register(
36
+ bit_len=8,
37
+ name="Bar",
38
+ address=0x02,
39
+ desc="Very important register",
40
+ model=[FieldDef.value_of("b1@[3:0]U4.1#ro"), FieldDef.value_of("b2@[5:4]S2.0#rw")],
41
+ ),
42
+ ]
43
+ )
44
+ ```
45
+
46
+ To add this library to your project do
47
+
48
+ ```shell
49
+ uv add rgscore
50
+ ```
51
+
@@ -0,0 +1,10 @@
1
+ rgscore/__init__.py,sha256=1pBK-h5SYK-imPk_VGgFBd7FiZGqpUlIGsRXQuAI4hs,146
2
+ rgscore/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ rgscore/model/register.py,sha256=aIjQWDElcborFTNXVtQDbRHLBlk7vJBkwtHbD4tBFwg,23005
4
+ rgscore/model/reglist.py,sha256=YMHvDaXZOlwTZpUiuPNm_w1K6DJJedw9QhqzoV4c1AI,5968
5
+ rgscore/model/sfix.py,sha256=-2QSD6MztIq6AcWmfRTyZRVoEYW591Hq-ycVmiS6vmI,675
6
+ rgscore-0.14.2.dist-info/licenses/LICENSE,sha256=UBiXDGNSvplE3Bx8SPXgZpdje80HPyWokuB6Tqq4EPg,1071
7
+ rgscore-0.14.2.dist-info/METADATA,sha256=SCNXwBd60OxNBNJ4HIcm05Qomk07uCGL0-XC7ZX5bjA,1570
8
+ rgscore-0.14.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
9
+ rgscore-0.14.2.dist-info/top_level.txt,sha256=YnGfUkhZWwW6x_lwk0i-QwGjZWKpLAme_jjaG6_A0Z8,8
10
+ rgscore-0.14.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitri Priimak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ rgscore