micrOSDevToolKit 2.17.2__py3-none-any.whl → 2.20.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 (73) hide show
  1. micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +29 -33
  2. micrOS/source/Common.py +5 -13
  3. micrOS/source/Config.py +2 -2
  4. micrOS/source/Espnow.py +32 -18
  5. micrOS/source/InterConnect.py +106 -31
  6. micrOS/source/Server.py +2 -3
  7. micrOS/source/Shell.py +1 -1
  8. micrOS/source/Tasks.py +66 -62
  9. micrOS/source/micrOSloader.py +1 -1
  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_espnow.py +18 -1
  15. micrOS/source/modules/LM_i2s_mic.py +1 -2
  16. micrOS/source/modules/LM_keychain.py +1 -2
  17. micrOS/source/modules/LM_light_sensor.py +1 -4
  18. micrOS/source/modules/LM_mqtt_client.py +13 -10
  19. micrOS/source/modules/LM_neoeffects.py +1 -1
  20. micrOS/source/modules/LM_neomatrix.py +1 -1
  21. micrOS/source/modules/LM_neopixel.py +1 -2
  22. micrOS/source/modules/LM_oled_ui.py +4 -3
  23. micrOS/source/modules/LM_oledui.py +6 -7
  24. micrOS/source/modules/LM_presence.py +1 -2
  25. micrOS/source/modules/LM_rest.py +1 -2
  26. micrOS/source/modules/LM_rgb.py +1 -2
  27. micrOS/source/modules/LM_roboarm.py +3 -4
  28. micrOS/source/modules/LM_robustness.py +1 -2
  29. micrOS/source/modules/LM_tcs3472.py +131 -17
  30. micrOS/source/modules/LM_telegram.py +17 -18
  31. {microsdevtoolkit-2.17.2.dist-info → microsdevtoolkit-2.20.0.dist-info}/METADATA +151 -215
  32. {microsdevtoolkit-2.17.2.dist-info → microsdevtoolkit-2.20.0.dist-info}/RECORD +69 -73
  33. toolkit/Gateway.py +1 -1
  34. toolkit/dashboard_apps/SystemTest.py +39 -32
  35. toolkit/micrOSdashboard.py +8 -13
  36. toolkit/socketClient.py +27 -7
  37. toolkit/workspace/precompiled/Common.mpy +0 -0
  38. toolkit/workspace/precompiled/Config.mpy +0 -0
  39. toolkit/workspace/precompiled/Espnow.mpy +0 -0
  40. toolkit/workspace/precompiled/InterConnect.mpy +0 -0
  41. toolkit/workspace/precompiled/Server.mpy +0 -0
  42. toolkit/workspace/precompiled/Shell.mpy +0 -0
  43. toolkit/workspace/precompiled/Tasks.mpy +0 -0
  44. toolkit/workspace/precompiled/micrOSloader.mpy +0 -0
  45. toolkit/workspace/precompiled/modules/LM_buzzer.mpy +0 -0
  46. toolkit/workspace/precompiled/modules/LM_cct.mpy +0 -0
  47. toolkit/workspace/precompiled/modules/LM_dimmer.mpy +0 -0
  48. toolkit/workspace/precompiled/modules/LM_distance.mpy +0 -0
  49. toolkit/workspace/precompiled/modules/LM_espnow.py +18 -1
  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_neoeffects.mpy +0 -0
  55. toolkit/workspace/precompiled/modules/LM_neomatrix.mpy +0 -0
  56. toolkit/workspace/precompiled/modules/LM_neopixel.mpy +0 -0
  57. toolkit/workspace/precompiled/modules/LM_oled_ui.mpy +0 -0
  58. toolkit/workspace/precompiled/modules/LM_oledui.mpy +0 -0
  59. toolkit/workspace/precompiled/modules/LM_presence.mpy +0 -0
  60. toolkit/workspace/precompiled/modules/LM_rest.mpy +0 -0
  61. toolkit/workspace/precompiled/modules/LM_rgb.mpy +0 -0
  62. toolkit/workspace/precompiled/modules/LM_roboarm.mpy +0 -0
  63. toolkit/workspace/precompiled/modules/LM_robustness.py +1 -2
  64. toolkit/workspace/precompiled/modules/LM_tcs3472.py +131 -17
  65. toolkit/workspace/precompiled/modules/LM_telegram.mpy +0 -0
  66. micrOS/micropython/esp32s2-LOLIN_MINI-20220618-v1.19.1.bin +0 -0
  67. micrOS/micropython/esp32s3_spiram_oct-20231005-v1.21.0.bin +0 -0
  68. micrOS/source/modules/LM_pet_feeder.py +0 -78
  69. toolkit/workspace/precompiled/modules/LM_pet_feeder.py +0 -78
  70. {microsdevtoolkit-2.17.2.data → microsdevtoolkit-2.20.0.data}/scripts/devToolKit.py +0 -0
  71. {microsdevtoolkit-2.17.2.dist-info → microsdevtoolkit-2.20.0.dist-info}/WHEEL +0 -0
  72. {microsdevtoolkit-2.17.2.dist-info → microsdevtoolkit-2.20.0.dist-info}/licenses/LICENSE +0 -0
  73. {microsdevtoolkit-2.17.2.dist-info → microsdevtoolkit-2.20.0.dist-info}/top_level.txt +0 -0
@@ -25,7 +25,7 @@
25
25
  1
26
26
  ],
27
27
  "Tasks.py": [
28
- 9.43,
28
+ 9.48,
29
29
  12
30
30
  ],
31
31
  "Config.py": [
@@ -46,7 +46,7 @@
46
46
  ],
47
47
  "Types.py": [
48
48
  8.91,
49
- 28
49
+ 29
50
50
  ],
51
51
  "Logger.py": [
52
52
  8.84,
@@ -54,10 +54,10 @@
54
54
  ],
55
55
  "Common.py": [
56
56
  9.85,
57
- 35
57
+ 34
58
58
  ],
59
59
  "InterConnect.py": [
60
- 9.41,
60
+ 9.62,
61
61
  2
62
62
  ],
63
63
  "Debug.py": [
@@ -69,8 +69,8 @@
69
69
  8
70
70
  ],
71
71
  "Espnow.py": [
72
- 9.5,
73
- 3
72
+ 9.47,
73
+ 4
74
74
  ],
75
75
  "Scheduler.py": [
76
76
  9.62,
@@ -97,7 +97,7 @@
97
97
  5
98
98
  ],
99
99
  "modules/LM_roboarm.py": [
100
- 7.78,
100
+ 7.83,
101
101
  0
102
102
  ],
103
103
  "modules/LM_stepper.py": [
@@ -117,7 +117,7 @@
117
117
  0
118
118
  ],
119
119
  "modules/LM_oled_ui.py": [
120
- 8.78,
120
+ 9.2,
121
121
  0
122
122
  ],
123
123
  "modules/LM_system.py": [
@@ -125,7 +125,7 @@
125
125
  0
126
126
  ],
127
127
  "modules/LM_robustness.py": [
128
- 8.36,
128
+ 8.33,
129
129
  0
130
130
  ],
131
131
  "modules/LM_qmi8658.py": [
@@ -137,11 +137,11 @@
137
137
  0
138
138
  ],
139
139
  "modules/LM_rest.py": [
140
- 8.18,
140
+ 8.6,
141
141
  0
142
142
  ],
143
143
  "modules/LM_tcs3472.py": [
144
- 9.0,
144
+ 9.48,
145
145
  0
146
146
  ],
147
147
  "modules/LM_oled.py": [
@@ -161,7 +161,7 @@
161
161
  0
162
162
  ],
163
163
  "modules/LM_buzzer.py": [
164
- 9.11,
164
+ 9.09,
165
165
  0
166
166
  ],
167
167
  "modules/LM_switch.py": [
@@ -189,11 +189,11 @@
189
189
  0
190
190
  ],
191
191
  "modules/LM_neopixel.py": [
192
- 7.78,
192
+ 7.77,
193
193
  0
194
194
  ],
195
195
  "modules/LM_cct.py": [
196
- 9.04,
196
+ 9.03,
197
197
  0
198
198
  ],
199
199
  "modules/LM_L9110_DCmotor.py": [
@@ -220,10 +220,6 @@
220
220
  7.62,
221
221
  0
222
222
  ],
223
- "modules/LM_pet_feeder.py": [
224
- 7.94,
225
- 0
226
- ],
227
223
  "modules/LM_rencoder.py": [
228
224
  8.65,
229
225
  0
@@ -245,15 +241,15 @@
245
241
  0
246
242
  ],
247
243
  "modules/LM_oledui.py": [
248
- 8.7,
244
+ 8.94,
249
245
  0
250
246
  ],
251
247
  "modules/LM_espnow.py": [
252
- 6.0,
248
+ 7.14,
253
249
  0
254
250
  ],
255
251
  "modules/LM_telegram.py": [
256
- 9.58,
252
+ 9.62,
257
253
  0
258
254
  ],
259
255
  "modules/LM_OV2640.py": [
@@ -265,7 +261,7 @@
265
261
  0
266
262
  ],
267
263
  "modules/LM_distance.py": [
268
- 8.87,
264
+ 8.86,
269
265
  0
270
266
  ],
271
267
  "modules/LM_VL53L0X.py": [
@@ -273,7 +269,7 @@
273
269
  0
274
270
  ],
275
271
  "modules/LM_light_sensor.py": [
276
- 9.17,
272
+ 9.12,
277
273
  0
278
274
  ],
279
275
  "modules/LM_rp2w.py": [
@@ -281,7 +277,7 @@
281
277
  0
282
278
  ],
283
279
  "modules/LM_presence.py": [
284
- 8.9,
280
+ 8.89,
285
281
  0
286
282
  ],
287
283
  "modules/LM_trackball.py": [
@@ -289,7 +285,7 @@
289
285
  0
290
286
  ],
291
287
  "modules/LM_mqtt_client.py": [
292
- 8.45,
288
+ 8.68,
293
289
  0
294
290
  ],
295
291
  "modules/LM_dashboard_be.py": [
@@ -297,7 +293,7 @@
297
293
  0
298
294
  ],
299
295
  "modules/LM_dimmer.py": [
300
- 8.33,
296
+ 8.32,
301
297
  0
302
298
  ],
303
299
  "modules/LM_gameOfLife.py": [
@@ -309,7 +305,7 @@
309
305
  0
310
306
  ],
311
307
  "modules/LM_i2s_mic.py": [
312
- 8.43,
308
+ 8.42,
313
309
  0
314
310
  ],
315
311
  "modules/LM_sdcard.py": [
@@ -323,12 +319,12 @@
323
319
  },
324
320
  "summary": {
325
321
  "core": [
326
- 3925,
322
+ 3994,
327
323
  24
328
324
  ],
329
325
  "load": [
330
- 9880,
331
- 56
326
+ 9912,
327
+ 55
332
328
  ],
333
329
  "core_dep": [
334
330
  true,
@@ -338,8 +334,8 @@
338
334
  true,
339
335
  4
340
336
  ],
341
- "core_score": 9.24,
342
- "load_score": 8.36,
343
- "version": "2.17.1-0"
337
+ "core_score": 9.25,
338
+ "load_score": 8.42,
339
+ "version": "2.20.0-0"
344
340
  }
345
341
  }
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
@@ -102,12 +102,12 @@ class Data:
102
102
  liveconf = Data.read_cfg_file(nosafe=True)
103
103
  # Remove obsolete keys from conf
104
104
  try:
105
- remove('cleanup.pds') # Try to remove cleanup.pds (cleanup indicator by micrOSloader)
105
+ remove('.cleanup') # Try to remove .cleanup (cleanup indicator by micrOSloader)
106
106
  console_write("[CONF] Purge obsolete keys")
107
107
  for key in (key for key in liveconf if key not in Data.CONFIG_CACHE):
108
108
  liveconf.pop(key, None)
109
109
  except Exception:
110
- console_write("[CONF] SKIP obsolete keys check (no cleanup.pds)")
110
+ console_write("[CONF] SKIP obsolete keys check (no .cleanup)")
111
111
  # Merge template to live conf (store active conf in Data.CONFIG_CACHE)
112
112
  Data.CONFIG_CACHE.update(liveconf)
113
113
  console_write("[CONF] User config injection done")
micrOS/source/Espnow.py CHANGED
@@ -99,14 +99,14 @@ class ESPNowSS:
99
99
  self.devfid = cfgget('devfid')
100
100
  self.devices: dict[bytes, str] = {} # mapping for { "mac address": "devfid" } pairs
101
101
  self.server_ready = False
102
- self.peer_cache = path_join(OSPath.DATA, "espnow_peers.app_json")
102
+ self.peer_cache_path = path_join(OSPath.DATA, "espnow_peers.app_json")
103
103
  self._load_peers()
104
104
 
105
105
  def _load_peers(self):
106
- if not is_file(self.peer_cache):
106
+ if not is_file(self.peer_cache_path):
107
107
  return
108
108
  try:
109
- with open(self.peer_cache, 'r') as f:
109
+ with open(self.peer_cache_path, 'r') as f:
110
110
  devices_map = load(f)
111
111
  self.devices = {bytes(k): v for k, v in devices_map}
112
112
  for mac in self.devices:
@@ -179,6 +179,7 @@ class ESPNowSS:
179
179
  self.server_ready = True
180
180
  my_task.out = "ESPNow receiver running"
181
181
  async for mac, msg in self.espnow:
182
+ reply, response = False, ""
182
183
  try:
183
184
  reply, response = self._request_handler(msg, my_task, mac)
184
185
  if reply:
@@ -188,7 +189,6 @@ class ESPNowSS:
188
189
  if len(err.args) > 1 and err.args[1] == 'ESP_ERR_ESPNOW_NOT_FOUND':
189
190
  # AUTOMATIC PEER REGISTRATION
190
191
  self.espnow.add_peer(mac)
191
- reply, response = self._request_handler(msg, my_task, mac)
192
192
  if reply:
193
193
  await self.__asend_raw(mac, response)
194
194
  else:
@@ -201,8 +201,7 @@ class ESPNowSS:
201
201
  """
202
202
  # Create an asynchronous task with tag 'espnow.server'
203
203
  tag = 'espnow.server'
204
- state = NativeTask().create(callback=self._server(tag), tag=tag)
205
- return {tag: "Starting"} if state else {tag: "Already running"}
204
+ return NativeTask().create(callback=self._server(tag), tag=tag)
206
205
 
207
206
  #----------- SEND METHODS --------------
208
207
  async def __asend_raw(self, mac:bytes, msg:str):
@@ -249,8 +248,7 @@ class ESPNowSS:
249
248
  peer_name = hexlify(peer, ':').decode() if peer_name is None else peer_name
250
249
  task_id = f"con.espnow.{peer_name}"
251
250
  # 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"}
251
+ return NativeTask().create(callback=self._asend_task(peer, task_id, msg), tag=task_id)
254
252
 
255
253
  def cluster_send(self, msg):
256
254
  """
@@ -262,6 +260,14 @@ class ESPNowSS:
262
260
  return _tasks
263
261
 
264
262
  # ----------- OTHER METHODS --------------
263
+ def save_peers(self):
264
+ try:
265
+ with open(self.peer_cache_path, "w") as f:
266
+ dump([[list(k), v] for k, v in self.devices.items()], f)
267
+ return True
268
+ except Exception as e:
269
+ syslog(f"[ERR][ESPNOW] Saving peers: {e}")
270
+ return False
265
271
 
266
272
  async def _handshake(self, peer:bytes, tag:str):
267
273
  """
@@ -285,16 +291,10 @@ class ESPNowSS:
285
291
  expected_response = f"hello {self.devfid}"
286
292
  is_ok = False
287
293
  if result == expected_response:
288
- try:
289
- with open(self.peer_cache, "w") as f:
290
- dump([[list(k), v] for k, v in self.devices.items()], f)
291
- is_ok = True
292
- except Exception as e:
293
- syslog(f"[ERR][ESPNOW] Saving peers: {e}")
294
+ is_ok = self.save_peers()
294
295
  my_task.out = f"Handshake: {result} from {self.devices.get(peer)} [{'OK' if is_ok else 'NOK'}]"
295
296
  sender_task.cancel() # Delete sender task (cleanup)
296
297
 
297
-
298
298
  def handshake(self, peer:bytes|str):
299
299
  task_id = f"con.espnow.handshake"
300
300
  # Create an asynchronous sending task.
@@ -302,8 +302,7 @@ class ESPNowSS:
302
302
  # Convert 50:02:91:86:34:28 format to b'P\x02\x91\x864(' bytes
303
303
  peer = bytes(int(x, 16) for x in peer.split(":"))
304
304
  if isinstance(peer, bytes):
305
- state = NativeTask().create(callback=self._handshake(peer, task_id), tag=task_id)
306
- return {task_id: "Starting"} if state else {task_id: "Already running"}
305
+ return NativeTask().create(callback=self._handshake(peer, task_id), tag=task_id)
307
306
  else:
308
307
  return {None: "Invalid MAC address format. Use 50:02:91:86:34:28 or b'P\\x02\\x91\\x864('"}
309
308
 
@@ -321,4 +320,19 @@ class ESPNowSS:
321
320
  _peers = self.espnow.peers_table
322
321
  except Exception as e:
323
322
  _peers = str(e)
324
- return {"stats": _stats, "peers": _peers, "map": self.devices, "ready": self.server_ready}
323
+ return {"stats": _stats, "peers": _peers, "ready": self.server_ready}
324
+
325
+ def members(self):
326
+ return self.devices
327
+
328
+ def remove_peer(self, peer:bytes) -> bool:
329
+ """
330
+ Remove peer from ESPNow devices
331
+ :param peer: MAC address as bytes to remove
332
+ """
333
+ if isinstance(peer, bytes):
334
+ if self.devices.pop(peer, None) is not None:
335
+ self.save_peers()
336
+ self.espnow.del_peer(peer)
337
+ return True
338
+ return False
@@ -1,14 +1,34 @@
1
+ """
2
+ Module is responsible for device-device communication
3
+ dedicated to micrOS framework.
4
+ Built-in-function:
5
+ - Socket InterConnect interface
6
+ - ESPNow redirection
7
+
8
+ Designed by
9
+ Marcell Ban aka BxNxM
10
+ Kristof Kasza aka KKristof452
11
+ """
12
+
1
13
  from socket import getaddrinfo, SOCK_STREAM
2
- from re import compile
14
+ from re import compile as re_compile
15
+ from json import loads
3
16
  from uasyncio import open_connection
17
+
4
18
  from Debug import syslog
5
19
  from Config import cfgget
6
20
  from Server import Server
7
21
  from Tasks import NativeTask
8
22
 
23
+ if cfgget('espnow'):
24
+ from Espnow import ESPNowSS
25
+ else:
26
+ ESPNowSS = None
27
+
9
28
 
10
29
  class InterCon:
11
- CONN_MAP = {}
30
+ CONN_MAP: dict[str, str] = {} # hostname: IP address pairs
31
+ NO_ESPNOW: list[str] = [] # disabled ESPNow hostname list (cache for fallback speed-up)
12
32
  PORT = cfgget('socport')
13
33
 
14
34
  def __init__(self):
@@ -19,7 +39,7 @@ class InterCon:
19
39
 
20
40
  @staticmethod
21
41
  def validate_ipv4(str_in):
22
- pattern = compile(r'^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$')
42
+ pattern = re_compile(r'^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$')
23
43
  return bool(pattern.match(str_in))
24
44
 
25
45
  async def send_cmd(self, host:str, cmd:list):
@@ -34,7 +54,12 @@ class InterCon:
34
54
  # Check if host is a hostname (example.local) and resolve its IP address
35
55
  if not InterCon.validate_ipv4(host):
36
56
  hostname = host
37
- # Retrieve IP address by hostname dynamically
57
+ # Lookup hostname without .domain (sub-hostname matching)
58
+ if '.' not in hostname:
59
+ _hosts = list([d for d in InterCon.CONN_MAP if hostname in d])
60
+ if len(_hosts) > 0:
61
+ hostname = _hosts[0]
62
+ # Retrieve IP address by hostname.domain dynamically
38
63
  if InterCon.CONN_MAP.get(hostname, None) is None:
39
64
  try:
40
65
  addr_info = getaddrinfo(host, InterCon.PORT, 0, SOCK_STREAM)
@@ -68,8 +93,7 @@ class InterCon:
68
93
  InterCon.CONN_MAP[hostname] = None if output is None else host
69
94
  # None: ServerBusy(or \0) or Prompt mismatch (auto delete cached IP), STR: valid comm. output
70
95
  return output
71
- else:
72
- syslog(f"[ERR][intercon] Invalid host: {host}")
96
+ syslog(f"[ERR][intercon] Invalid host: {host}")
73
97
  return ''
74
98
 
75
99
  async def __run_command(self, cmd:list, hostname:str):
@@ -85,7 +109,6 @@ class InterCon:
85
109
  # Compare prompt |node01 $| with hostname 'node01.local'
86
110
  if hostname is None or prompt is None or str(prompt).replace('$', '').strip() == str(hostname).split('.')[0]:
87
111
  # Run command on validated device
88
- # TODO: Handle multiple cmd as input, separated by ; (????)
89
112
  self.writer.write(str.encode(' '.join(cmd)))
90
113
  await self.writer.drain()
91
114
  data, _ = await self.__receive_data(prompt=prompt)
@@ -109,7 +132,7 @@ class InterCon:
109
132
  return True # AuthOk
110
133
  return False # AuthFailed
111
134
 
112
- async def __receive_data(self, prompt=None):
135
+ async def __receive_data(self, prompt=None) -> tuple[str, str]:
113
136
  """
114
137
  Implements data receive loop until prompt / [configure] / Bye!
115
138
  :param prompt: socket shell prompt
@@ -139,8 +162,34 @@ class InterCon:
139
162
  data = data.replace(prompt, '').replace('\n', ' ')
140
163
  return data, prompt
141
164
 
165
+ async def auto_espnow_handshake(self, host:str) -> dict:
166
+ """
167
+ [1] Check espnow.server running on host
168
+ [2] Get MAC address for host (from system info)
169
+ [3] Execute ESPNowSS.handshake
170
+ """
171
+ response = await self.send_cmd(host, ["task", "list", ">json"])
172
+ if not response:
173
+ return {None: f"[ERR] ESPNow auto handshake: task list, {response}"}
174
+
175
+ active_tasks = loads(response).get("active")
176
+ if "espnow.server" in active_tasks:
177
+ response = await self.send_cmd(host, ["system", "info", ">json"])
178
+ if not response:
179
+ return {None: "[ERR] ESPNow auto handshake: system info"}
180
+ try:
181
+ host_mac = loads(response).get("mac")
182
+ except Exception as ex:
183
+ return {None: f"[ERR] ESPNow auto handshake: {ex}"}
184
+
185
+ return ESPNowSS().handshake(host_mac)
186
+
187
+ if not InterCon.validate_ipv4(host):
188
+ InterCon.NO_ESPNOW.append(str(host).split(".")[0]) # host.local -> host
189
+ return {None: f"ESPNow auto handshake: espnow disabled on host {host}"}
190
+
142
191
 
143
- async def _send_cmd(host:str, cmd:list, com_obj):
192
+ async def _socket_send_cmd(host:str, cmd:list, com_obj:InterCon) -> None:
144
193
  """
145
194
  Async send command wrapper for further async task integration and sync send_cmd usage (main)
146
195
  :param host: hostname / IP address
@@ -148,21 +197,57 @@ async def _send_cmd(host:str, cmd:list, com_obj):
148
197
  :param com_obj: InterCon object to utilize send_cmd method and task status updates
149
198
  """
150
199
  # Send command
200
+ for _ in range(0, 2): # Retry mechanism
201
+ out = await com_obj.send_cmd(host, cmd) # Send CMD
202
+ if out is not None: # Retry mechanism
203
+ break
204
+ await com_obj.task.feed(sleep_ms=100) # Retry mechanism
205
+ com_obj.task.out = '' if out is None else out
206
+
207
+
208
+ async def _send_cmd(host:str, cmd:list|str, com_obj:InterCon):
209
+ """
210
+ Top level InterConnect callback function
211
+ [1] node01.domain -> ESPNow, Socket
212
+ (domain: .local, .net, etc.)
213
+ [2] node01 -> ESPNow, Socket fallback in case found in InterConnect cache
214
+ [3] IP address -> Socket
215
+ """
151
216
  with com_obj.task:
152
- for _ in range(0, 4): # Retry mechanism
153
- out = await com_obj.send_cmd(host, cmd) # Send CMD
154
- if out is not None: # Retry mechanism
155
- break
156
- await com_obj.task.feed(sleep_ms=100) # Retry mechanism
157
- com_obj.task.out = '' if out is None else out
158
- return com_obj.task.out
217
+ if ESPNowSS:
218
+ # [1] ESPNow Active
219
+ name = str(host).split(".")[0] # host.local -> host
220
+ if name not in InterCon.NO_ESPNOW and name in list(ESPNowSS().devices.values()):
221
+ com_obj.task.out = "Redirected to ESPNow"
222
+ if isinstance(cmd, list):
223
+ cmd = ' '.join(cmd)
224
+ # Send command and retrieve result
225
+ sender = ESPNowSS().send(peer=name, msg=cmd)
226
+ sender_task = NativeTask.TASKS.get(list(sender.keys())[0])
227
+ result = await sender_task.await_result(timeout=10)
228
+ if result != "Timeout has beed exceeded":
229
+ # Successful command execution
230
+ com_obj.task.out = result # Output mirroring: Child -> Parent
231
+ sender_task.out = "Redirected to ParentTask" # Remove redundant data in embedded mode
232
+ return
233
+
234
+ # Handle legacy string input
235
+ if isinstance(cmd, str):
236
+ cmd = cmd.split()
237
+ # [1][2] Socket send (default and fallback)
238
+ await _socket_send_cmd(host, cmd, com_obj)
239
+
240
+ if ESPNowSS and name not in InterCon.NO_ESPNOW:
241
+ # [3] Automatic ESPNow handshake
242
+ verdict = await com_obj.auto_espnow_handshake(host)
243
+ if list(verdict.keys())[0] is None:
244
+ syslog(str(list(verdict.values())[0]))
159
245
 
160
246
 
161
247
  def send_cmd(host:str, cmd:list|str) -> dict:
162
248
  """
163
- Sync wrapper of async _send_cmd (InterCon.send_cmd consumer with retry)
164
- :param host: hostname / IP address
165
- :param cmd: command string to server socket shell
249
+ Top level InterConnect send task creation
250
+ Handles ESPNow and socket communication
166
251
  """
167
252
  def _tagify():
168
253
  nonlocal host, cmd
@@ -171,18 +256,9 @@ def send_cmd(host:str, cmd:list|str) -> dict:
171
256
  return f"{'.'.join(host.split('.')[-2:])}.{_mod}"
172
257
  return f"{host.replace('.local', '')}.{_mod}"
173
258
 
174
- # Handle legacy string input
175
- if isinstance(cmd, str):
176
- cmd = cmd.split()
177
-
178
259
  com_obj = InterCon()
179
- tag = f"con.{_tagify()}"
180
- started = com_obj.task.create(callback=_send_cmd(host, cmd, com_obj), tag=tag)
181
- if started:
182
- result = {"verdict": f"Task started: task show {tag}", "tag": tag}
183
- else:
184
- result = {"verdict": "Task is Busy", "tag": tag}
185
- return result
260
+ task_id = f"con.{_tagify()}" # CHECK TASK ID CONFLICT
261
+ return com_obj.task.create(callback=_send_cmd(host, cmd, com_obj), tag=task_id)
186
262
 
187
263
 
188
264
  def host_cache() -> dict:
@@ -190,4 +266,3 @@ def host_cache() -> dict:
190
266
  Dump InterCon connection cache
191
267
  """
192
268
  return InterCon.CONN_MAP
193
-
micrOS/source/Server.py CHANGED
@@ -183,12 +183,11 @@ class WebCli(Client, WebEngine):
183
183
  :param method: HTTP method name
184
184
  :param auto_enable: enable webui when register (endpoint)
185
185
  """
186
- if method not in WebEngine.METHODS:
187
- raise ValueError(f"method must be one of {WebEngine.METHODS}")
188
-
189
186
  if cfgget('webui'):
190
187
  if not endpoint in WebEngine.ENDPOINTS:
191
188
  WebEngine.ENDPOINTS[endpoint] = {}
189
+ if method not in WebEngine.METHODS:
190
+ raise ValueError(f"method must be one of {WebEngine.METHODS}")
192
191
  WebEngine.ENDPOINTS[endpoint][method] = callback
193
192
  return
194
193
  # AUTO ENABLE webui when register (endpoint) called and webui is False
micrOS/source/Shell.py CHANGED
@@ -25,7 +25,7 @@ from Debug import syslog
25
25
 
26
26
  class Shell:
27
27
  __slots__ = ['__devfid', '__auth_mode', '__hwuid', '__auth_ok', '__conf_mode']
28
- MICROS_VERSION = '2.17.1-0'
28
+ MICROS_VERSION = '2.20.0-0'
29
29
 
30
30
  def __init__(self):
31
31
  """