micrOSDevToolKit 2.19.0__py3-none-any.whl → 2.21.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 micrOSDevToolKit might be problematic. Click here for more details.

Files changed (69) hide show
  1. micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +35 -35
  2. micrOS/source/Common.py +5 -13
  3. micrOS/source/Config.py +5 -2
  4. micrOS/source/Espnow.py +101 -45
  5. micrOS/source/Files.py +50 -23
  6. micrOS/source/InterConnect.py +10 -5
  7. micrOS/source/Pacman.py +141 -0
  8. micrOS/source/Shell.py +1 -1
  9. micrOS/source/Tasks.py +59 -54
  10. micrOS/source/modules/LM_buzzer.py +1 -4
  11. micrOS/source/modules/LM_cct.py +2 -4
  12. micrOS/source/modules/LM_dimmer.py +1 -2
  13. micrOS/source/modules/LM_distance.py +1 -3
  14. micrOS/source/modules/LM_i2s_mic.py +1 -2
  15. micrOS/source/modules/LM_keychain.py +1 -2
  16. micrOS/source/modules/LM_light_sensor.py +1 -4
  17. micrOS/source/modules/LM_mqtt_client.py +13 -10
  18. micrOS/source/modules/LM_neopixel.py +1 -2
  19. micrOS/source/modules/LM_oled_ui.py +39 -41
  20. micrOS/source/modules/LM_oledui.py +58 -89
  21. micrOS/source/modules/LM_pacman.py +36 -44
  22. micrOS/source/modules/LM_presence.py +1 -2
  23. micrOS/source/modules/LM_rest.py +1 -2
  24. micrOS/source/modules/LM_rgb.py +1 -2
  25. micrOS/source/modules/LM_roboarm.py +3 -4
  26. micrOS/source/modules/LM_robustness.py +1 -2
  27. micrOS/source/modules/LM_telegram.py +1 -2
  28. micrOS/source/urequests.py +1 -1
  29. {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/METADATA +153 -214
  30. {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/RECORD +67 -67
  31. toolkit/DevEnvOTA.py +2 -2
  32. toolkit/DevEnvUSB.py +1 -1
  33. toolkit/dashboard_apps/SystemTest.py +17 -14
  34. toolkit/lib/micrOSClient.py +37 -15
  35. toolkit/lib/micrOSClientHistory.py +35 -1
  36. toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
  37. toolkit/simulator_lib/uos.py +1 -0
  38. toolkit/workspace/precompiled/Common.mpy +0 -0
  39. toolkit/workspace/precompiled/Config.mpy +0 -0
  40. toolkit/workspace/precompiled/Espnow.mpy +0 -0
  41. toolkit/workspace/precompiled/Files.mpy +0 -0
  42. toolkit/workspace/precompiled/InterConnect.mpy +0 -0
  43. toolkit/workspace/precompiled/Pacman.mpy +0 -0
  44. toolkit/workspace/precompiled/Shell.mpy +0 -0
  45. toolkit/workspace/precompiled/Tasks.mpy +0 -0
  46. toolkit/workspace/precompiled/modules/LM_buzzer.mpy +0 -0
  47. toolkit/workspace/precompiled/modules/LM_cct.mpy +0 -0
  48. toolkit/workspace/precompiled/modules/LM_dimmer.mpy +0 -0
  49. toolkit/workspace/precompiled/modules/LM_distance.mpy +0 -0
  50. toolkit/workspace/precompiled/modules/LM_i2s_mic.mpy +0 -0
  51. toolkit/workspace/precompiled/modules/LM_keychain.mpy +0 -0
  52. toolkit/workspace/precompiled/modules/LM_light_sensor.mpy +0 -0
  53. toolkit/workspace/precompiled/modules/LM_mqtt_client.mpy +0 -0
  54. toolkit/workspace/precompiled/modules/LM_neopixel.mpy +0 -0
  55. toolkit/workspace/precompiled/modules/LM_oled_ui.mpy +0 -0
  56. toolkit/workspace/precompiled/modules/LM_oledui.mpy +0 -0
  57. toolkit/workspace/precompiled/modules/LM_pacman.mpy +0 -0
  58. toolkit/workspace/precompiled/modules/LM_presence.mpy +0 -0
  59. toolkit/workspace/precompiled/modules/LM_rest.mpy +0 -0
  60. toolkit/workspace/precompiled/modules/LM_rgb.mpy +0 -0
  61. toolkit/workspace/precompiled/modules/LM_roboarm.mpy +0 -0
  62. toolkit/workspace/precompiled/modules/LM_robustness.py +1 -2
  63. toolkit/workspace/precompiled/modules/LM_telegram.mpy +0 -0
  64. micrOS/source/modules/LM_pet_feeder.py +0 -78
  65. toolkit/workspace/precompiled/modules/LM_pet_feeder.py +0 -78
  66. {microsdevtoolkit-2.19.0.data → microsdevtoolkit-2.21.0.data}/scripts/devToolKit.py +0 -0
  67. {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/WHEEL +0 -0
  68. {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/licenses/LICENSE +0 -0
  69. {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/top_level.txt +0 -0
@@ -4,9 +4,13 @@
4
4
  9.34,
5
5
  6
6
6
  ],
7
+ "Pacman.py": [
8
+ 9.75,
9
+ 1
10
+ ],
7
11
  "Files.py": [
8
- 9.64,
9
- 10
12
+ 9.68,
13
+ 11
10
14
  ],
11
15
  "micrOSloader.py": [
12
16
  7.06,
@@ -25,11 +29,11 @@
25
29
  1
26
30
  ],
27
31
  "Tasks.py": [
28
- 9.46,
32
+ 9.48,
29
33
  12
30
34
  ],
31
35
  "Config.py": [
32
- 9.43,
36
+ 9.44,
33
37
  19
34
38
  ],
35
39
  "reset.py": [
@@ -54,22 +58,22 @@
54
58
  ],
55
59
  "Common.py": [
56
60
  9.85,
57
- 35
61
+ 34
58
62
  ],
59
63
  "InterConnect.py": [
60
- 9.62,
64
+ 9.63,
61
65
  2
62
66
  ],
63
67
  "Debug.py": [
64
68
  7.37,
65
- 17
69
+ 18
66
70
  ],
67
71
  "Network.py": [
68
72
  9.81,
69
73
  8
70
74
  ],
71
75
  "Espnow.py": [
72
- 9.48,
76
+ 9.87,
73
77
  4
74
78
  ],
75
79
  "Scheduler.py": [
@@ -97,7 +101,7 @@
97
101
  5
98
102
  ],
99
103
  "modules/LM_roboarm.py": [
100
- 7.78,
104
+ 7.83,
101
105
  0
102
106
  ],
103
107
  "modules/LM_stepper.py": [
@@ -109,7 +113,7 @@
109
113
  0
110
114
  ],
111
115
  "modules/LM_pacman.py": [
112
- 7.95,
116
+ 7.61,
113
117
  0
114
118
  ],
115
119
  "modules/LM_genIO.py": [
@@ -117,7 +121,7 @@
117
121
  0
118
122
  ],
119
123
  "modules/LM_oled_ui.py": [
120
- 9.2,
124
+ 9.22,
121
125
  0
122
126
  ],
123
127
  "modules/LM_system.py": [
@@ -125,7 +129,7 @@
125
129
  0
126
130
  ],
127
131
  "modules/LM_robustness.py": [
128
- 8.36,
132
+ 8.33,
129
133
  0
130
134
  ],
131
135
  "modules/LM_qmi8658.py": [
@@ -137,7 +141,7 @@
137
141
  0
138
142
  ],
139
143
  "modules/LM_rest.py": [
140
- 8.18,
144
+ 8.6,
141
145
  0
142
146
  ],
143
147
  "modules/LM_tcs3472.py": [
@@ -161,7 +165,7 @@
161
165
  0
162
166
  ],
163
167
  "modules/LM_buzzer.py": [
164
- 9.11,
168
+ 9.09,
165
169
  0
166
170
  ],
167
171
  "modules/LM_switch.py": [
@@ -189,11 +193,11 @@
189
193
  0
190
194
  ],
191
195
  "modules/LM_neopixel.py": [
192
- 7.78,
196
+ 7.77,
193
197
  0
194
198
  ],
195
199
  "modules/LM_cct.py": [
196
- 9.04,
200
+ 9.03,
197
201
  0
198
202
  ],
199
203
  "modules/LM_L9110_DCmotor.py": [
@@ -220,10 +224,6 @@
220
224
  7.62,
221
225
  0
222
226
  ],
223
- "modules/LM_pet_feeder.py": [
224
- 7.94,
225
- 0
226
- ],
227
227
  "modules/LM_rencoder.py": [
228
228
  8.65,
229
229
  0
@@ -245,7 +245,7 @@
245
245
  0
246
246
  ],
247
247
  "modules/LM_oledui.py": [
248
- 8.94,
248
+ 9.05,
249
249
  0
250
250
  ],
251
251
  "modules/LM_espnow.py": [
@@ -253,7 +253,7 @@
253
253
  0
254
254
  ],
255
255
  "modules/LM_telegram.py": [
256
- 9.63,
256
+ 9.62,
257
257
  0
258
258
  ],
259
259
  "modules/LM_OV2640.py": [
@@ -265,7 +265,7 @@
265
265
  0
266
266
  ],
267
267
  "modules/LM_distance.py": [
268
- 8.87,
268
+ 8.86,
269
269
  0
270
270
  ],
271
271
  "modules/LM_VL53L0X.py": [
@@ -273,7 +273,7 @@
273
273
  0
274
274
  ],
275
275
  "modules/LM_light_sensor.py": [
276
- 9.17,
276
+ 9.12,
277
277
  0
278
278
  ],
279
279
  "modules/LM_rp2w.py": [
@@ -281,7 +281,7 @@
281
281
  0
282
282
  ],
283
283
  "modules/LM_presence.py": [
284
- 8.9,
284
+ 8.89,
285
285
  0
286
286
  ],
287
287
  "modules/LM_trackball.py": [
@@ -289,7 +289,7 @@
289
289
  0
290
290
  ],
291
291
  "modules/LM_mqtt_client.py": [
292
- 8.45,
292
+ 8.68,
293
293
  0
294
294
  ],
295
295
  "modules/LM_dashboard_be.py": [
@@ -297,7 +297,7 @@
297
297
  0
298
298
  ],
299
299
  "modules/LM_dimmer.py": [
300
- 8.33,
300
+ 8.32,
301
301
  0
302
302
  ],
303
303
  "modules/LM_gameOfLife.py": [
@@ -309,7 +309,7 @@
309
309
  0
310
310
  ],
311
311
  "modules/LM_i2s_mic.py": [
312
- 8.43,
312
+ 8.42,
313
313
  0
314
314
  ],
315
315
  "modules/LM_sdcard.py": [
@@ -323,12 +323,12 @@
323
323
  },
324
324
  "summary": {
325
325
  "core": [
326
- 4004,
327
- 24
326
+ 4171,
327
+ 25
328
328
  ],
329
329
  "load": [
330
- 9988,
331
- 56
330
+ 9869,
331
+ 55
332
332
  ],
333
333
  "core_dep": [
334
334
  true,
@@ -338,8 +338,8 @@
338
338
  true,
339
339
  4
340
340
  ],
341
- "core_score": 9.25,
342
- "load_score": 8.4,
343
- "version": "2.19.0-0"
341
+ "core_score": 9.29,
342
+ "load_score": 8.41,
343
+ "version": "2.21.0-0"
344
344
  }
345
345
  }
micrOS/source/Common.py CHANGED
@@ -15,7 +15,7 @@ from Notify import Notify
15
15
  #####################################################################################
16
16
  # SYSTEM #
17
17
  #####################################################################################
18
- def micro_task(tag: str, task=None, _wrap=False):
18
+ def micro_task(tag:str, task=None, _wrap=False):
19
19
  """
20
20
  [LM] Async task manager.
21
21
  Modes:
@@ -37,21 +37,14 @@ def micro_task(tag: str, task=None, _wrap=False):
37
37
  """
38
38
  # --- CREATE (original) ---
39
39
  if task is not None:
40
- if TaskBase.is_busy(tag):
41
- return None # task already running
42
40
  return Manager().create_task(callback=task, tag=tag)
43
41
 
44
42
  # --- CREATE WITH DECORATOR FACTORY (simplified) ---
45
43
  if _wrap:
46
44
  def _decorator(async_fn):
47
45
  task_tag = f"{tag}._{async_fn.__name__}"
48
- _launcher = (
49
- lambda *args, **kwargs:
50
- None if TaskBase.is_busy(task_tag)
51
- else Manager().create_task(
52
- callback=async_fn(task_tag, *args, **kwargs),
53
- tag=task_tag)
54
- )
46
+ _launcher = (lambda *args, **kwargs: Manager().create_task(callback=async_fn(task_tag, *args, **kwargs),
47
+ tag=task_tag))
55
48
  return _launcher
56
49
  return _decorator
57
50
 
@@ -404,9 +397,8 @@ class AnimationPlayer:
404
397
  # Ensure async loop set up correctly. (After stop operation, it is needed)
405
398
  self.__running = True
406
399
  # [!] ASYNC TASK CREATION
407
- raw_state:bool = micro_task(tag=self._task_tag, task=self._player())
408
- state = "starting" if raw_state else "running"
409
- settings["state"] = state
400
+ state:dict = micro_task(tag=self._task_tag, task=self._player())
401
+ settings["state"] = list(state.values())[0]
410
402
  return settings
411
403
 
412
404
  def stop(self):
micrOS/source/Config.py CHANGED
@@ -84,8 +84,11 @@ class Data:
84
84
  Data.__inject_default_conf()
85
85
  # [!!!] Init selected pinmap - default pinmap calculated by platform
86
86
  if callable(set_pinmap):
87
- pinmap = set_pinmap(Data.CONFIG_CACHE['cstmpmap'])
88
- console_write(f"[PIN MAP] {pinmap}")
87
+ try:
88
+ pinmap = set_pinmap(Data.CONFIG_CACHE['cstmpmap'])
89
+ console_write(f"[PIN MAP] {pinmap}")
90
+ except Exception as e:
91
+ console_write(f"\n[PIN MAP] !!! SETUP ERROR !!!: {e}\n")
89
92
  # SET dbg based on config settings (inject user conf)
90
93
  DebugCfg.DEBUG = Data.CONFIG_CACHE['dbg']
91
94
  if DebugCfg.DEBUG:
micrOS/source/Espnow.py CHANGED
@@ -1,7 +1,23 @@
1
- from aioespnow import AIOESPNow
1
+ """
2
+ ESPNow Session Server and Protocol Utilities
3
+
4
+ This module implements:
5
+ - Custom ESPNow message protocol with transaction IDs (tid) for secure, session-aware communication.
6
+ - Asynchronous server and client logic for sending and receiving ESPNow messages.
7
+ - Response routing using both MAC address and transaction ID to ensure correct delivery to tasks.
8
+ - Peer management, handshake routines, and statistics reporting for ESPNow devices.
9
+
10
+ Designed for MicroPython environments with async support.
11
+ """
12
+
13
+
2
14
  from binascii import hexlify
3
15
  from json import load, dump
4
16
  import uasyncio as asyncio
17
+ import urandom
18
+
19
+ from aioespnow import AIOESPNow
20
+
5
21
  from Tasks import NativeTask, lm_exec, lm_is_loaded
6
22
  from Config import cfgget
7
23
  from Debug import syslog
@@ -10,7 +26,7 @@ from Files import OSPath, path_join, is_file
10
26
 
11
27
  # ----------- PARSE AND RENDER MSG PROTOCOL --------------
12
28
 
13
- def render_response(tid:str, oper:str, data:str, prompt:str) -> str:
29
+ def render_packet(tid: str, oper: str, data: str, prompt: str) -> str:
14
30
  """
15
31
  Render ESPNow custom message (protocol)
16
32
  """
@@ -21,7 +37,7 @@ def render_response(tid:str, oper:str, data:str, prompt:str) -> str:
21
37
  .replace("{data}", str(data)).replace("{prompt}", prompt))
22
38
  return tmp
23
39
 
24
- def parse_request(msg:bytes) -> (bool, dict|str):
40
+ def parse_packet(msg: bytes) -> tuple[bool, dict | str]:
25
41
  """
26
42
  Parse ESPNow custom message protocol
27
43
  """
@@ -39,21 +55,43 @@ def parse_request(msg:bytes) -> (bool, dict|str):
39
55
  return False, f"Missing 4 args: {msg}"
40
56
 
41
57
 
58
+ def get_command_module(request):
59
+ if isinstance(request, dict):
60
+ command = request["data"].split()
61
+ module = command[0]
62
+ elif isinstance(request, str):
63
+ command = request.split()
64
+ module = command[0]
65
+ else:
66
+ command = []
67
+ module = ""
68
+ return command, module
69
+
70
+
71
+ def generate_tid() -> str:
72
+ """
73
+ Generate a secure, random transaction ID (tid).
74
+ Returns an 8-byte hex string.
75
+ """
76
+ return hexlify(bytes([urandom.getrandbits(8) for _ in range(8)])).decode()
77
+
78
+
42
79
  # ----------- ESPNOW SESSION SERVER - LISTENER AND SENDER --------------
43
80
  class ResponseRouter:
44
81
  """
45
82
  Response Router (by mac address)
46
83
  to connect sender task with receiver loop (aka server)
47
84
  """
48
- _routes: dict[bytes, "ResponseRouter"] = {}
85
+ _routes: dict[tuple[bytes, str], "ResponseRouter"] = {}
49
86
 
50
- def __init__(self, mac: bytes):
87
+ def __init__(self, mac: bytes, tid: str):
51
88
  self.mac = mac
89
+ self.tid = tid
52
90
  self.response = None
53
91
  self._event = asyncio.Event()
54
- ResponseRouter._routes[mac] = self
92
+ ResponseRouter._routes[(mac, tid)] = self
55
93
 
56
- async def get_response(self, timeout:int=10) -> str|dict:
94
+ async def get_response(self, timeout: int=10) -> str|dict:
57
95
  """Wait for one response, then clear the event for reuse."""
58
96
  try:
59
97
  await asyncio.wait_for(self._event.wait(), timeout)
@@ -63,18 +101,17 @@ class ResponseRouter:
63
101
  return self.response
64
102
 
65
103
  @staticmethod
66
- def update_response(mac: bytes, response: str) -> None:
67
- # USE <tid> for proper session response mapping
68
- router = ResponseRouter._routes.get(mac)
104
+ def update_response(mac: bytes, tid: str, response: str) -> None:
105
+ router = ResponseRouter._routes.get((mac, tid), None)
69
106
  if router is None:
70
- syslog(f"[WARN][ESPNOW] No response route for {mac}")
107
+ syslog(f"[WARN][ESPNOW] No response route for {(mac, tid)}")
71
108
  return
72
109
  router.response = response
73
110
  router._event.set()
74
111
 
75
112
  def close(self) -> None:
76
113
  """Remove routing entry when done."""
77
- ResponseRouter._routes.pop(self.mac, None)
114
+ ResponseRouter._routes.pop((self.mac, self.tid), None)
78
115
 
79
116
 
80
117
  class ESPNowSS:
@@ -106,7 +143,7 @@ class ESPNowSS:
106
143
  if not is_file(self.peer_cache_path):
107
144
  return
108
145
  try:
109
- with open(self.peer_cache_path, 'r') as f:
146
+ with open(self.peer_cache_path, 'r', encoding='utf-8') as f:
110
147
  devices_map = load(f)
111
148
  self.devices = {bytes(k): v for k, v in devices_map}
112
149
  for mac in self.devices:
@@ -116,7 +153,7 @@ class ESPNowSS:
116
153
  syslog(f"[ERR][ESPNOW] Loading peers: {e}")
117
154
 
118
155
  # ----------- SERVER METHODS --------------
119
- def _request_handler(self, msg:bytes, my_task:NativeTask, mac:bytes):
156
+ def _request_handler(self, msg: bytes, my_task: NativeTask, mac: bytes):
120
157
  """
121
158
  Handle server input message (request), with REQ/RSP types (oper)
122
159
  oper==REQ - command execution
@@ -126,7 +163,7 @@ class ESPNowSS:
126
163
  :param mac: sender binary mac address
127
164
  """
128
165
 
129
- state, request = parse_request(msg)
166
+ state, request = parse_packet(msg)
130
167
  if not state:
131
168
  my_task.out = f"[_ESPNOW] {request}"
132
169
  return state, request
@@ -139,18 +176,17 @@ class ESPNowSS:
139
176
 
140
177
  # Check if the module/command is allowed., check oper==REQ/RSP
141
178
  if operation == "REQ":
142
- command = request["data"].split()
143
- module = command[0]
179
+ command, module = get_command_module(request)
144
180
  # Handle default hello - handshake message
145
181
  if len(command) == 1 and module == "hello":
146
- rendered_out = render_response(tid="?", oper="RSP", data=f"hello {prompt}", prompt=self.devfid)
182
+ rendered_out = render_packet(tid=tid, oper="RSP", data=f"hello {prompt}", prompt=self.devfid)
147
183
  return True, rendered_out
148
184
  # COMMAND EXECUTION
149
185
  if lm_is_loaded(module):
150
186
  try:
151
187
  state, out = lm_exec(command)
152
188
  # rendered_output: "{tid}|{oper}|{data}|{prompt}$"
153
- rendered_out = render_response(tid="?", oper="RSP", data=out, prompt=self.devfid)
189
+ rendered_out = render_packet(tid=tid, oper="RSP", data=out, prompt=self.devfid)
154
190
  return state, rendered_out
155
191
  except Exception as e:
156
192
  # Optionally log the exception here.
@@ -159,17 +195,17 @@ class ESPNowSS:
159
195
  else:
160
196
  warning_msg = f"[WARN][_ESPNOW] NotAllowed {module}"
161
197
  syslog(warning_msg)
162
- rendered_out = render_response(tid="?", oper="RSP", data=warning_msg,
198
+ rendered_out = render_packet(tid=tid, oper="RSP", data=warning_msg,
163
199
  prompt=self.devfid)
164
200
  state, out = True, rendered_out
165
201
  return state, out
166
202
  if operation == "RSP":
167
203
  resp_data = request["data"]
168
- ResponseRouter.update_response(mac, resp_data) # USE <tid> for proper session response mapping
204
+ ResponseRouter.update_response(mac, tid, resp_data) # USE <tid> for proper session response mapping
169
205
  #syslog(f"[_ESPNOW] No action, {request}")
170
206
  return False, ""
171
207
 
172
- async def _server(self, tag:str):
208
+ async def _server(self, tag: str):
173
209
  """
174
210
  ESPnow async listener task
175
211
  :param tag: micro_task tag for task access
@@ -201,24 +237,23 @@ class ESPNowSS:
201
237
  """
202
238
  # Create an asynchronous task with tag 'espnow.server'
203
239
  tag = 'espnow.server'
204
- state = NativeTask().create(callback=self._server(tag), tag=tag)
205
- return {tag: "Starting"} if state else {tag: "Already running"}
240
+ return NativeTask().create(callback=self._server(tag), tag=tag)
206
241
 
207
242
  #----------- SEND METHODS --------------
208
- async def __asend_raw(self, mac:bytes, msg:str):
243
+ async def __asend_raw(self, mac: bytes, msg: str):
209
244
  """
210
245
  ESPnow send message to mac address
211
246
  """
212
247
  return await self.espnow.asend(mac, msg.encode("utf-8"))
213
248
 
214
- async def _asend_task(self, peer:bytes, tag:str, msg:str):
249
+ async def _asend_task(self, tid: str, peer: bytes, tag: str, msg: str):
215
250
  """
216
251
  ESPNow client task: send a command to a peer and update task status.
217
252
  """
218
253
  with NativeTask.TASKS.get(tag, None) as my_task:
219
- router = ResponseRouter(peer)
254
+ router = ResponseRouter(peer, tid)
220
255
  # rendered_output: "{tid}|{oper}|{data}|{prompt}$"
221
- rendered_out = render_response(tid="?", oper="REQ", data=msg, prompt=self.devfid)
256
+ rendered_out = render_packet(tid=tid, oper="REQ", data=msg, prompt=self.devfid)
222
257
  if await self.__asend_raw(peer, rendered_out):
223
258
  my_task.out = f"[ESPNOW SEND] {rendered_out}"
224
259
  my_task.out = await router.get_response()
@@ -226,12 +261,12 @@ class ESPNowSS:
226
261
  my_task.out = "[ESPNOW SEND] Peer not responding"
227
262
  router.close()
228
263
 
229
- def mac_by_peer_name(self, peer_name:str) -> bytes|None:
264
+ def mac_by_peer_name(self, peer_name: str) -> bytes | None:
230
265
  matches = [k for k, v in self.devices.items() if v == peer_name]
231
266
  peer = matches[0] if matches else None
232
267
  return peer
233
268
 
234
- def send(self, peer:bytes|str, msg:str) -> dict:
269
+ def send(self, peer: bytes | str, msg: str) -> dict:
235
270
  """
236
271
  Send a command over ESPNow.
237
272
  :param peer: Binary MAC address of another device.
@@ -247,10 +282,11 @@ class ESPNowSS:
247
282
  peer = _peer
248
283
 
249
284
  peer_name = hexlify(peer, ':').decode() if peer_name is None else peer_name
250
- task_id = f"con.espnow.{peer_name}"
285
+ _, module_name = get_command_module(msg)
286
+ task_id = f"con.espnow.{peer_name}.{module_name}"
287
+ tid = generate_tid()
251
288
  # Create an asynchronous sending task.
252
- state = NativeTask().create(callback=self._asend_task(peer, task_id, msg), tag=task_id)
253
- return {task_id: "Starting"} if state else {task_id: "Already running"}
289
+ return NativeTask().create(callback=self._asend_task(tid, peer, task_id, msg), tag=task_id)
254
290
 
255
291
  def cluster_send(self, msg):
256
292
  """
@@ -264,14 +300,14 @@ class ESPNowSS:
264
300
  # ----------- OTHER METHODS --------------
265
301
  def save_peers(self):
266
302
  try:
267
- with open(self.peer_cache_path, "w") as f:
303
+ with open(self.peer_cache_path, "w", encoding="utf-8") as f:
268
304
  dump([[list(k), v] for k, v in self.devices.items()], f)
269
305
  return True
270
306
  except Exception as e:
271
307
  syslog(f"[ERR][ESPNOW] Saving peers: {e}")
272
308
  return False
273
309
 
274
- async def _handshake(self, peer:bytes, tag:str):
310
+ async def _handshake(self, peer: bytes, tag: str):
275
311
  """
276
312
  Handshake with peer
277
313
  - with device caching
@@ -297,17 +333,34 @@ class ESPNowSS:
297
333
  my_task.out = f"Handshake: {result} from {self.devices.get(peer)} [{'OK' if is_ok else 'NOK'}]"
298
334
  sender_task.cancel() # Delete sender task (cleanup)
299
335
 
300
- def handshake(self, peer:bytes|str):
301
- task_id = f"con.espnow.handshake"
336
+ def _mac_str_to_bytes(self, mac_str: str) -> bytes|None:
337
+ """
338
+ Convert MAC address string (e.g., '50:02:91:86:34:28') to bytes.
339
+ """
340
+ try:
341
+ mac_bytes = bytes(int(x, 16) for x in mac_str.split(":"))
342
+ if len(mac_bytes) != 6:
343
+ return None
344
+ return mac_bytes
345
+ except Exception:
346
+ return None
347
+
348
+ def handshake(self, peer: bytes | str):
349
+ """
350
+ Initiate a handshake with a peer device over ESPNow.
351
+
352
+ :param peer: The peer device's MAC address as bytes or a string in the format '50:02:91:86:34:28'.
353
+ :return: A dictionary with error information or a NativeTask instance for the handshake operation.
354
+ """
355
+ task_id = "con.espnow.handshake"
302
356
  # Create an asynchronous sending task.
303
357
  if isinstance(peer, str) and ":" in peer:
304
- # Convert 50:02:91:86:34:28 format to b'P\x02\x91\x864(' bytes
305
- peer = bytes(int(x, 16) for x in peer.split(":"))
358
+ peer_bytes = self._mac_str_to_bytes(peer)
359
+ if peer_bytes is not None:
360
+ peer = peer_bytes
306
361
  if isinstance(peer, bytes):
307
- state = NativeTask().create(callback=self._handshake(peer, task_id), tag=task_id)
308
- return {task_id: "Starting"} if state else {task_id: "Already running"}
309
- else:
310
- return {None: "Invalid MAC address format. Use 50:02:91:86:34:28 or b'P\\x02\\x91\\x864('"}
362
+ return NativeTask().create(callback=self._handshake(peer, task_id), tag=task_id)
363
+ return {None: "Invalid MAC address format. Use 50:02:91:86:34:28 or b'P\\x02\\x91\\x864('"}
311
364
 
312
365
  def stats(self):
313
366
  """
@@ -324,11 +377,14 @@ class ESPNowSS:
324
377
  except Exception as e:
325
378
  _peers = str(e)
326
379
  return {"stats": _stats, "peers": _peers, "ready": self.server_ready}
327
-
380
+
328
381
  def members(self):
382
+ """
383
+ Returns the list of devices that are members of the current group.
384
+ """
329
385
  return self.devices
330
386
 
331
- def remove_peer(self, peer:bytes) -> bool:
387
+ def remove_peer(self, peer: bytes) -> bool:
332
388
  """
333
389
  Remove peer from ESPNow devices
334
390
  :param peer: MAC address as bytes to remove