ekfsm 1.3.0a26__py3-none-any.whl → 1.4.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.

Potentially problematic release.


This version of ekfsm might be problematic. Click here for more details.

@@ -0,0 +1,206 @@
1
+ import threading
2
+ from ekfsm.devices.button import Button
3
+ from ekfsm.devices.generic import Device
4
+ from ekfsm.devices.io4edge import IO4Edge
5
+ from ekfsm.log import ekfsm_logger
6
+ from io4edge_client.binaryiotypeb import Client, Pb
7
+ import io4edge_client.functionblock as fb
8
+
9
+ logger = ekfsm_logger(__name__)
10
+
11
+
12
+ class ButtonArray(Device):
13
+ """
14
+ Device class for handling an io4edge button array.
15
+
16
+ To read button events, call the `read` method in a separate thread.
17
+
18
+ Note
19
+ ----
20
+ Button handlers are called in the context of the `read` method's thread and need to be set in the Button instances.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ name: str,
26
+ parent: IO4Edge,
27
+ children: list[Device] | None = None,
28
+ abort: bool = False,
29
+ service_suffix: str | None = None,
30
+ keepaliveInterval: int = 10000,
31
+ *args,
32
+ **kwargs,
33
+ ):
34
+ logger.debug(
35
+ f"Initializing ButtonArray '{name}' with parent device {parent.deviceId}"
36
+ )
37
+
38
+ Device.__init__(self, name, parent, children, abort, *args, **kwargs)
39
+
40
+ self.name = name
41
+
42
+ if service_suffix is not None:
43
+ self.service_suffix = service_suffix
44
+ logger.debug(f"Using custom service suffix: {service_suffix}")
45
+ else:
46
+ self.service_suffix = name
47
+ logger.debug(f"Using default service suffix: {name}")
48
+
49
+ self.service_addr = f"{parent.deviceId}-{self.service_suffix}"
50
+ self.timeout = keepaliveInterval / 1000 + 5
51
+
52
+ logger.info(
53
+ f"ButtonArray '{name}' configured with service address: {self.service_addr}"
54
+ )
55
+ logger.debug(
56
+ f"Keepalive interval: {keepaliveInterval}ms, timeout: {self.timeout}s"
57
+ )
58
+
59
+ try:
60
+ self.client = Client(
61
+ self.service_addr, command_timeout=self.timeout, connect=False
62
+ )
63
+ logger.debug(f"IO4Edge client created for service: {self.service_addr}")
64
+ except Exception as e:
65
+ logger.error(
66
+ f"Failed to create IO4Edge client for {self.service_addr}: {e}"
67
+ )
68
+ raise
69
+
70
+ self.subscriptionType = Pb.SubscriptionType.BINARYIOTYPEB_ON_RISING_EDGE
71
+ self.stream_cfg = fb.Pb.StreamControlStart(
72
+ bucketSamples=1, # 1 sample per bucket, also ein event pro bucket
73
+ keepaliveInterval=keepaliveInterval,
74
+ bufferedSamples=2, # 2 samples werden gepuffert
75
+ low_latency_mode=True, # schickt soweit moeglich sofort die Events
76
+ )
77
+ logger.debug(
78
+ "Stream configuration initialized with rising edge subscription and low latency mode"
79
+ )
80
+
81
+ # Log button children count
82
+ button_count = sum(1 for child in (children or []) if isinstance(child, Button))
83
+ logger.info(f"ButtonArray '{name}' initialized with {button_count} button(s)")
84
+
85
+ def read(self, stop_event: threading.Event | None = None, timeout: float = 1):
86
+ """
87
+ Read all button events and dispatch to handlers.
88
+
89
+ Parameters
90
+ ----------
91
+ stop_event : threading.Event, optional
92
+ Event to signal stopping the reading loop. If None, the loop will run indefinitely.
93
+ timeout : float, optional
94
+ Timeout for reading from the stream in seconds. Default is 0.1 seconds.
95
+
96
+ Note
97
+ ----
98
+ This method blocks and should be run in a separate thread.
99
+ """
100
+ button_channels = [
101
+ button for button in self.children if isinstance(button, Button)
102
+ ]
103
+
104
+ if not button_channels:
105
+ logger.warning(
106
+ f"No button children found in ButtonArray '{self.name}', read operation will have no effect"
107
+ )
108
+ return
109
+
110
+ logger.info(
111
+ f"Starting button event reading for {len(button_channels)} buttons on '{self.name}'"
112
+ )
113
+ logger.debug(
114
+ f"Read timeout: {timeout}s, stop_event provided: {stop_event is not None}"
115
+ )
116
+
117
+ try:
118
+ with self.client as client:
119
+ logger.debug(
120
+ f"IO4Edge client connected to service: {self.service_addr}"
121
+ )
122
+
123
+ # Prepare subscription channels
124
+ subscribe_channels = tuple(
125
+ Pb.SubscribeChannel(
126
+ channel=button.channel_id,
127
+ subscriptionType=self.subscriptionType,
128
+ )
129
+ for button in button_channels
130
+ )
131
+
132
+ channel_ids = [button.channel_id for button in button_channels]
133
+ logger.debug(
134
+ f"Subscribing to {len(subscribe_channels)} button channels: {channel_ids}"
135
+ )
136
+
137
+ client.start_stream(
138
+ Pb.StreamControlStart(subscribeChannel=subscribe_channels),
139
+ self.stream_cfg,
140
+ )
141
+ logger.info(
142
+ f"Button event stream started for ButtonArray '{self.name}'"
143
+ )
144
+
145
+ event_count = 0
146
+ try:
147
+ while not (stop_event and stop_event.is_set()):
148
+ try:
149
+ _, samples = client.read_stream(timeout=timeout)
150
+
151
+ for sample in samples.samples:
152
+ for button in button_channels:
153
+ pressed = bool(
154
+ sample.inputs & (1 << button.channel_id)
155
+ )
156
+ if pressed:
157
+ event_count += 1
158
+ button_name = getattr(button, "name", "unnamed")
159
+ logger.debug(
160
+ f"Button press on channel {button.channel_id} ({button_name})"
161
+ )
162
+
163
+ if button.handler:
164
+ try:
165
+ logger.debug(
166
+ f"Calling handler for button on channel {button.channel_id}"
167
+ )
168
+ button.handler()
169
+ except Exception as e:
170
+ logger.error(
171
+ f"Error in button handler for channel {button.channel_id}: {e}"
172
+ )
173
+ else:
174
+ logger.debug(
175
+ f"No handler set for button on channel {button.channel_id}"
176
+ )
177
+
178
+ except TimeoutError:
179
+ # Timeout is expected during normal operation
180
+ continue
181
+ except Exception as e:
182
+ logger.error(
183
+ f"Error reading button events from stream: {e}"
184
+ )
185
+ break
186
+
187
+ except KeyboardInterrupt:
188
+ logger.info(
189
+ f"Button reading interrupted for ButtonArray '{self.name}'"
190
+ )
191
+ finally:
192
+ logger.info(
193
+ f"Button event reading stopped for '{self.name}' after processing {event_count} events"
194
+ )
195
+ if stop_event:
196
+ stop_event.clear()
197
+ logger.debug("Stop event cleared")
198
+
199
+ except Exception as e:
200
+ logger.error(
201
+ f"Failed to establish connection or start stream for ButtonArray '{self.name}': {e}"
202
+ )
203
+ raise
204
+
205
+ def __repr__(self):
206
+ return f"{self.name}; Service Address: {self.service_addr}"
@@ -0,0 +1,101 @@
1
+ from ekfsm.devices.generic import Device
2
+ from ekfsm.devices.ledArray import LEDArray
3
+ from ekfsm.log import ekfsm_logger
4
+ from io4edge_client.api.colorLED.python.colorLED.v1alpha1.colorLED_pb2 import Color
5
+
6
+ logger = ekfsm_logger(__name__)
7
+
8
+
9
+ class ColorLED(Device):
10
+ """
11
+ Device class for handling a color LED.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ name: str,
17
+ parent: LEDArray,
18
+ children: list[Device] | None = None,
19
+ abort: bool = False,
20
+ channel_id: int = 0,
21
+ *args,
22
+ **kwargs,
23
+ ):
24
+ logger.debug(f"Initializing ColorLED '{name}' on channel {channel_id}")
25
+
26
+ super().__init__(name, parent, children, abort, *args, **kwargs)
27
+
28
+ self.name = name
29
+ self.channel_id = channel_id
30
+
31
+ self.client = parent.client
32
+ logger.info(
33
+ f"ColorLED '{name}' initialized on channel {channel_id} with parent LEDArray"
34
+ )
35
+
36
+ def describe(self):
37
+ pass
38
+
39
+ def get(self) -> tuple[Color, bool]:
40
+ """
41
+ Get color LED state.
42
+
43
+ Returns
44
+ -------
45
+ Current color and blink state.
46
+
47
+ Raises
48
+ ------
49
+ RuntimeError
50
+ if the command fails
51
+ TimeoutError
52
+ if the command times out
53
+ """
54
+ logger.debug(
55
+ f"Getting color LED state for '{self.name}' on channel {self.channel_id}"
56
+ )
57
+ try:
58
+ result = self.client.get(self.channel_id)
59
+ color, blink = result
60
+ logger.debug(f"ColorLED '{self.name}' state: color={color}, blink={blink}")
61
+ return result
62
+ except Exception as e:
63
+ logger.error(
64
+ f"Failed to get ColorLED '{self.name}' state on channel {self.channel_id}: {e}"
65
+ )
66
+ raise
67
+
68
+ def set(self, color: Color, blink: bool) -> None:
69
+ """
70
+ Set the color of the color LED.
71
+
72
+ Parameters
73
+ ----------
74
+ color : Color
75
+ The color to set the LED to.
76
+ blink : bool
77
+ Whether to blink the LED.
78
+
79
+ Raises
80
+ ------
81
+ RuntimeError
82
+ if the command fails
83
+ TimeoutError
84
+ if the command times out
85
+ """
86
+ logger.info(
87
+ f"Setting ColorLED '{self.name}' on channel {self.channel_id}: color={color}, blink={blink}"
88
+ )
89
+ try:
90
+ self.client.set(self.channel_id, color, blink)
91
+ logger.debug(
92
+ f"ColorLED '{self.name}' successfully set to color={color}, blink={blink}"
93
+ )
94
+ except Exception as e:
95
+ logger.error(
96
+ f"Failed to set ColorLED '{self.name}' on channel {self.channel_id}: {e}"
97
+ )
98
+ raise
99
+
100
+ def __repr__(self):
101
+ return f"{self.name}; Channel ID: {self.channel_id}"
ekfsm/devices/coretemp.py CHANGED
@@ -7,6 +7,9 @@ from pathlib import Path
7
7
  import ekfsm.core
8
8
  from ekfsm.core.sysfs import sysfs_root
9
9
  from ekfsm.devices.generic import Device
10
+ from ekfsm.log import ekfsm_logger
11
+
12
+ logger = ekfsm_logger(__name__)
10
13
 
11
14
  # Path to the root of the HWMON sysfs filesystem
12
15
  HWMON_ROOT = sysfs_root() / Path("class/hwmon")
@@ -55,8 +58,16 @@ class CoreTemp(Device):
55
58
  *args,
56
59
  **kwargs,
57
60
  ):
58
- dir = find_core_temp_dir(sysfs_root() / Path("class/hwmon"))
59
- self.sysfs_device = ekfsm.core.sysfs.SysfsDevice(dir, False)
61
+ logger.debug(f"Initializing CoreTemp device '{name}'")
62
+
63
+ try:
64
+ dir = find_core_temp_dir(sysfs_root() / Path("class/hwmon"))
65
+ logger.debug(f"Found coretemp directory: {dir}")
66
+ self.sysfs_device = ekfsm.core.sysfs.SysfsDevice(dir, False)
67
+ logger.info(f"CoreTemp '{name}' initialized with sysfs device at {dir}")
68
+ except FileNotFoundError as e:
69
+ logger.error(f"Failed to initialize CoreTemp '{name}': {e}")
70
+ raise
60
71
 
61
72
  super().__init__(name, parent, None, abort, *args, **kwargs)
62
73
 
@@ -69,4 +80,16 @@ class CoreTemp(Device):
69
80
  int
70
81
  The CPU temperature in degrees Celsius.
71
82
  """
72
- return self.sysfs.read_int("temp1_input") / 1000
83
+ logger.debug(f"Reading CPU temperature for CoreTemp '{self.name}'")
84
+ try:
85
+ temp_raw = self.sysfs.read_float("temp1_input")
86
+ temp_celsius = temp_raw / 1000
87
+ logger.debug(
88
+ f"CoreTemp '{self.name}' raw reading: {temp_raw}, temperature: {temp_celsius}°C"
89
+ )
90
+ return temp_celsius
91
+ except Exception as e:
92
+ logger.error(
93
+ f"Failed to read CPU temperature for CoreTemp '{self.name}': {e}"
94
+ )
95
+ raise
ekfsm/devices/eeprom.py CHANGED
@@ -520,8 +520,8 @@ class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
520
520
  The date the device was manufactured.
521
521
  """
522
522
  area = self._content[self._date_mft_index_start : self._date_mft_index_end]
523
- encoded_mft_date = area[::-1]
524
- return self._decode_date(encoded_mft_date)
523
+ # encoded_mft_date = area[::-1]
524
+ return self._decode_date(area)
525
525
 
526
526
  @validated
527
527
  def repaired_at(self) -> date:
@@ -533,8 +533,8 @@ class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
533
533
  The most recent date the device was repaired.
534
534
  """
535
535
  area = self._content[self._date_rep_index_start : self._date_rep_index_end]
536
- encoded_rep_date = area[::-1]
537
- return self._decode_date(encoded_rep_date)
536
+ # encoded_rep_date = area[::-1]
537
+ return self._decode_date(area)
538
538
 
539
539
  @validated
540
540
  def write_repaired_at(self, date: date) -> None:
ekfsm/devices/generic.py CHANGED
@@ -63,7 +63,9 @@ class Device(SysTree):
63
63
  try:
64
64
  func = list(interface.values())[0]
65
65
  except IndexError:
66
- raise ConfigError(f"{self.name}: No function given for interface {name}.")
66
+ raise ConfigError(
67
+ f"{self.name}: No function given for interface {name}."
68
+ )
67
69
 
68
70
  if not hasattr(self, func):
69
71
  if abort:
@@ -114,10 +116,14 @@ class Device(SysTree):
114
116
  return self.sysfs.read_float(attr)
115
117
  case "int":
116
118
  return self.sysfs.read_int(attr)
119
+ case "hex":
120
+ return self.sysfs.read_hex(attr)
117
121
  case _:
118
122
  raise UnsupportedModeError(f"Mode {mode} is not supported")
119
123
 
120
- def read_attr_or_default(self, attr: str, mode: str = "utf", strip: bool = True, default=None):
124
+ def read_attr_or_default(
125
+ self, attr: str, mode: str = "utf", strip: bool = True, default=None
126
+ ):
121
127
  try:
122
128
  return self.read_attr(attr, mode, strip)
123
129
  except UnsupportedModeError:
@@ -149,7 +155,11 @@ class Device(SysTree):
149
155
  None:
150
156
  If the sysfs device is not set or the attribute does not exist.
151
157
  """
152
- if self.sysfs_device is not None and len(attr) != 0 and attr in [x.name for x in self.sysfs_device.attributes]:
158
+ if (
159
+ self.sysfs_device is not None
160
+ and len(attr) != 0
161
+ and attr in [x.name for x in self.sysfs_device.attributes]
162
+ ):
153
163
  return self.sysfs_device.read_attr_bytes(attr)
154
164
  return None
155
165
 
@@ -222,7 +232,9 @@ class Device(SysTree):
222
232
 
223
233
  chip_addr = self.device_args.get("addr")
224
234
  if chip_addr is None:
225
- raise ConfigError(f"{self.name}: Chip address not provided in board definition")
235
+ raise ConfigError(
236
+ f"{self.name}: Chip address not provided in board definition"
237
+ )
226
238
 
227
239
  if not hasattr(self.parent, "sysfs_device") or self.parent.sysfs_device is None:
228
240
  # our device is the top level device of the slot
@@ -230,12 +242,16 @@ class Device(SysTree):
230
242
  slot_attributes = self.hw_module.slot.attributes
231
243
 
232
244
  if slot_attributes is None:
233
- raise ConfigError(f"{self.name}: Slot attributes not provided in system configuration")
245
+ raise ConfigError(
246
+ f"{self.name}: Slot attributes not provided in system configuration"
247
+ )
234
248
 
235
249
  if not self.hw_module.is_master:
236
250
  # slot coding is only used for non-master devices
237
251
  if not hasattr(slot_attributes, "slot_coding"):
238
- raise ConfigError(f"{self.name}: Slot coding not provided in slot attributes")
252
+ raise ConfigError(
253
+ f"{self.name}: Slot coding not provided in slot attributes"
254
+ )
239
255
 
240
256
  slot_coding_mask = 0xFF
241
257
 
@@ -246,7 +262,9 @@ class Device(SysTree):
246
262
 
247
263
  return chip_addr
248
264
 
249
- def get_i2c_sysfs_device(self, addr: int, driver_required=True, find_driver: Callable | None = None) -> SysfsDevice:
265
+ def get_i2c_sysfs_device(
266
+ self, addr: int, driver_required=True, find_driver: Callable | None = None
267
+ ) -> SysfsDevice:
250
268
  from ekfsm.core.components import HWModule
251
269
 
252
270
  parent = self.parent
@@ -275,8 +293,12 @@ class Device(SysTree):
275
293
  # regular I2C devices that follow the `${I2C_BUS}-${ADDR}` pattern. To address this issue, we
276
294
  # initialize the ACPI _STR object for each PRP device with the necessary information, which is
277
295
  # accessible in the `${DEVICE_SYSFS_PATH}/firmware_node/description` file.
278
- if (entry / "firmware_node").exists() and (entry / "firmware_node" / "description").exists():
279
- description = (entry / "firmware_node/description").read_text().strip()
296
+ if (entry / "firmware_node").exists() and (
297
+ entry / "firmware_node" / "description"
298
+ ).exists():
299
+ description = (
300
+ (entry / "firmware_node/description").read_text().strip()
301
+ )
280
302
  acpi_addr = int(description.split(" - ")[0], 16)
281
303
 
282
304
  if acpi_addr == addr:
@@ -289,11 +311,16 @@ class Device(SysTree):
289
311
  if acpi_addr == addr:
290
312
  return SysfsDevice(entry, driver_required, find_driver)
291
313
 
292
- raise FileNotFoundError(f"Device with address 0x{addr:x} not found in {i2c_bus_path}")
314
+ raise FileNotFoundError(
315
+ f"Device with address 0x{addr:x} not found in {i2c_bus_path}"
316
+ )
293
317
 
294
318
  @staticmethod
295
319
  def __master_i2c_get_config(master: "HWModule") -> dict:
296
- if master.config.get("bus_masters") is not None and master.config["bus_masters"].get("i2c") is not None:
320
+ if (
321
+ master.config.get("bus_masters") is not None
322
+ and master.config["bus_masters"].get("i2c") is not None
323
+ ):
297
324
  return master.config["bus_masters"]["i2c"]
298
325
  else:
299
326
  raise ConfigError("Master definition incomplete")
@@ -310,7 +337,9 @@ class Device(SysTree):
310
337
  else:
311
338
  # another board is the master
312
339
  if self.hw_module.slot.master is None:
313
- raise ConfigError(f"{self.name}: Master board not found in slot attributes")
340
+ raise ConfigError(
341
+ f"{self.name}: Master board not found in slot attributes"
342
+ )
314
343
 
315
344
  master = self.hw_module.slot.master
316
345
  master_key = self.hw_module.slot.slot_type.name
@@ -0,0 +1,110 @@
1
+ from typing import Callable, Optional
2
+ from ekfsm.core.components import HWModule
3
+ from ekfsm.devices.generic import Device
4
+ from ekfsm.log import ekfsm_logger
5
+ import io4edge_client.core.coreclient as Client
6
+
7
+ from re import sub
8
+
9
+ logger = ekfsm_logger(__name__)
10
+
11
+
12
+ class IO4Edge(Device):
13
+ """
14
+ Device class for handling IO4Edge devices.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ name: str,
20
+ parent: HWModule | None = None,
21
+ children: list[Device] | None = None,
22
+ abort: bool = False,
23
+ *args,
24
+ **kwargs,
25
+ ):
26
+ logger.debug(f"Initializing IO4Edge device '{name}'")
27
+
28
+ super().__init__(name, parent, children, abort, *args, **kwargs)
29
+
30
+ attr = self.hw_module.slot.attributes
31
+ if (
32
+ attr is None
33
+ or not hasattr(attr, "slot_coding")
34
+ or getattr(attr, "slot_coding") is None
35
+ ):
36
+ logger.error(
37
+ f"Slot attributes for {self.hw_module.slot.name} are not set or do not contain 'slot_coding'"
38
+ )
39
+ raise ValueError(
40
+ f"Slot attributes for {self.hw_module.slot.name} are not set or do not contain 'slot_coding'."
41
+ )
42
+ else:
43
+ geoaddr = int(attr.slot_coding)
44
+ self._geoaddr = geoaddr
45
+ logger.debug(f"IO4Edge '{name}' geo address: {geoaddr}")
46
+
47
+ _, module_name = sub(r"-.*$", "", self.hw_module.board_type).split(maxsplit=1)
48
+ self._module_name = module_name
49
+ logger.debug(f"IO4Edge '{name}' module name: {module_name}")
50
+
51
+ try:
52
+ self.client = Client.new_core_client(self.deviceId)
53
+ logger.info(f"IO4Edge '{name}' initialized with device ID: {self.deviceId}")
54
+ except Exception as e:
55
+ logger.error(f"Failed to create IO4Edge core client for '{name}': {e}")
56
+ raise
57
+
58
+ @property
59
+ def deviceId(self) -> str:
60
+ """
61
+ Returns the device ID for the IO4Edge device.
62
+ The device ID is a combination of the module name and the geo address.
63
+ """
64
+ return f"{self._module_name}-geo_addr{self._geoaddr:02d}"
65
+
66
+ def identify_firmware(self) -> tuple[str, str]:
67
+ response = self.client.identify_firmware()
68
+ return (
69
+ response.title,
70
+ response.version,
71
+ )
72
+
73
+ def load_firmware(
74
+ self, cfg: bytes, progress_callback: Optional[Callable[[float], None]] = None
75
+ ) -> None:
76
+ """
77
+ Load firmware onto the IO4Edge device.
78
+
79
+ cfg
80
+ Firmware configuration bytes.
81
+ progress_callback
82
+ Optional callback for progress updates.
83
+ """
84
+ self.client.load_firmware(cfg, progress_callback)
85
+
86
+ def restart(self) -> None:
87
+ self.client.restart()
88
+
89
+ def load_parameter(self, name: str, value: str) -> None:
90
+ """
91
+ Set a parameter onto the IO4Edge device.
92
+
93
+ cfg
94
+ The name of the parameter to load.
95
+ value
96
+ The value to set for the parameter.
97
+ """
98
+ self.client.set_persistent_parameter(name, value)
99
+
100
+ def get_parameter(self, name: str) -> str:
101
+ """
102
+ Get a parameter value from the IO4Edge device.
103
+
104
+ Returns
105
+ The value of the requested parameter.
106
+ """
107
+ return self.client.get_persistent_parameter(name)
108
+
109
+ def __repr__(self):
110
+ return f"{self.name}; DeviceId: {self.deviceId}"
@@ -0,0 +1,54 @@
1
+ from ekfsm.devices.generic import Device
2
+ from ekfsm.devices.io4edge import IO4Edge
3
+ from ekfsm.log import ekfsm_logger
4
+ from io4edge_client.colorLED import Client
5
+
6
+ logger = ekfsm_logger(__name__)
7
+
8
+
9
+ class LEDArray(Device):
10
+ """
11
+ Device class for handling a LED array.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ name: str,
17
+ parent: IO4Edge,
18
+ children: list[Device] | None = None,
19
+ abort: bool = False,
20
+ service_suffix: str | None = None,
21
+ *args,
22
+ **kwargs,
23
+ ):
24
+ logger.debug(
25
+ f"Initializing LEDArray '{name}' with parent device {parent.deviceId}"
26
+ )
27
+
28
+ super().__init__(name, parent, children, abort, *args, **kwargs)
29
+
30
+ self.name = name
31
+
32
+ if service_suffix is not None:
33
+ self.service_suffix = service_suffix
34
+ logger.debug(f"Using custom service suffix: {service_suffix}")
35
+ else:
36
+ self.service_suffix = name
37
+ logger.debug(f"Using default service suffix: {name}")
38
+
39
+ self.service_addr = f"{parent.deviceId}-{self.service_suffix}"
40
+ logger.info(
41
+ f"LEDArray '{name}' configured with service address: {self.service_addr}"
42
+ )
43
+
44
+ try:
45
+ self.client = Client(self.service_addr)
46
+ logger.debug(f"LEDArray client created for service: {self.service_addr}")
47
+ except Exception as e:
48
+ logger.error(
49
+ f"Failed to create LEDArray client for {self.service_addr}: {e}"
50
+ )
51
+ raise
52
+
53
+ def __repr__(self):
54
+ return f"{self.name}; Service Address: {self.service_addr}"