micrOSDevToolKit 2.20.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 (34) hide show
  1. micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +19 -15
  2. micrOS/source/Config.py +5 -2
  3. micrOS/source/Espnow.py +99 -40
  4. micrOS/source/Files.py +50 -23
  5. micrOS/source/InterConnect.py +9 -3
  6. micrOS/source/Pacman.py +141 -0
  7. micrOS/source/Shell.py +1 -1
  8. micrOS/source/Tasks.py +4 -1
  9. micrOS/source/modules/LM_oled_ui.py +39 -41
  10. micrOS/source/modules/LM_oledui.py +56 -85
  11. micrOS/source/modules/LM_pacman.py +36 -44
  12. micrOS/source/urequests.py +1 -1
  13. {microsdevtoolkit-2.20.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/METADATA +9 -6
  14. {microsdevtoolkit-2.20.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/RECORD +34 -32
  15. toolkit/DevEnvOTA.py +2 -2
  16. toolkit/DevEnvUSB.py +1 -1
  17. toolkit/lib/micrOSClient.py +37 -15
  18. toolkit/lib/micrOSClientHistory.py +35 -1
  19. toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
  20. toolkit/simulator_lib/uos.py +1 -0
  21. toolkit/workspace/precompiled/Config.mpy +0 -0
  22. toolkit/workspace/precompiled/Espnow.mpy +0 -0
  23. toolkit/workspace/precompiled/Files.mpy +0 -0
  24. toolkit/workspace/precompiled/InterConnect.mpy +0 -0
  25. toolkit/workspace/precompiled/Pacman.mpy +0 -0
  26. toolkit/workspace/precompiled/Shell.mpy +0 -0
  27. toolkit/workspace/precompiled/Tasks.mpy +0 -0
  28. toolkit/workspace/precompiled/modules/LM_oled_ui.mpy +0 -0
  29. toolkit/workspace/precompiled/modules/LM_oledui.mpy +0 -0
  30. toolkit/workspace/precompiled/modules/LM_pacman.mpy +0 -0
  31. {microsdevtoolkit-2.20.0.data → microsdevtoolkit-2.21.0.data}/scripts/devToolKit.py +0 -0
  32. {microsdevtoolkit-2.20.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/WHEEL +0 -0
  33. {microsdevtoolkit-2.20.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/licenses/LICENSE +0 -0
  34. {microsdevtoolkit-2.20.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,
@@ -29,7 +33,7 @@
29
33
  12
30
34
  ],
31
35
  "Config.py": [
32
- 9.43,
36
+ 9.44,
33
37
  19
34
38
  ],
35
39
  "reset.py": [
@@ -57,19 +61,19 @@
57
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.47,
76
+ 9.87,
73
77
  4
74
78
  ],
75
79
  "Scheduler.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": [
@@ -241,7 +245,7 @@
241
245
  0
242
246
  ],
243
247
  "modules/LM_oledui.py": [
244
- 8.94,
248
+ 9.05,
245
249
  0
246
250
  ],
247
251
  "modules/LM_espnow.py": [
@@ -319,11 +323,11 @@
319
323
  },
320
324
  "summary": {
321
325
  "core": [
322
- 3994,
323
- 24
326
+ 4171,
327
+ 25
324
328
  ],
325
329
  "load": [
326
- 9912,
330
+ 9869,
327
331
  55
328
332
  ],
329
333
  "core_dep": [
@@ -334,8 +338,8 @@
334
338
  true,
335
339
  4
336
340
  ],
337
- "core_score": 9.25,
338
- "load_score": 8.42,
339
- "version": "2.20.0-0"
341
+ "core_score": 9.29,
342
+ "load_score": 8.41,
343
+ "version": "2.21.0-0"
340
344
  }
341
345
  }
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
@@ -204,20 +240,20 @@ class ESPNowSS:
204
240
  return NativeTask().create(callback=self._server(tag), tag=tag)
205
241
 
206
242
  #----------- SEND METHODS --------------
207
- async def __asend_raw(self, mac:bytes, msg:str):
243
+ async def __asend_raw(self, mac: bytes, msg: str):
208
244
  """
209
245
  ESPnow send message to mac address
210
246
  """
211
247
  return await self.espnow.asend(mac, msg.encode("utf-8"))
212
248
 
213
- 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):
214
250
  """
215
251
  ESPNow client task: send a command to a peer and update task status.
216
252
  """
217
253
  with NativeTask.TASKS.get(tag, None) as my_task:
218
- router = ResponseRouter(peer)
254
+ router = ResponseRouter(peer, tid)
219
255
  # rendered_output: "{tid}|{oper}|{data}|{prompt}$"
220
- 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)
221
257
  if await self.__asend_raw(peer, rendered_out):
222
258
  my_task.out = f"[ESPNOW SEND] {rendered_out}"
223
259
  my_task.out = await router.get_response()
@@ -225,12 +261,12 @@ class ESPNowSS:
225
261
  my_task.out = "[ESPNOW SEND] Peer not responding"
226
262
  router.close()
227
263
 
228
- def mac_by_peer_name(self, peer_name:str) -> bytes|None:
264
+ def mac_by_peer_name(self, peer_name: str) -> bytes | None:
229
265
  matches = [k for k, v in self.devices.items() if v == peer_name]
230
266
  peer = matches[0] if matches else None
231
267
  return peer
232
268
 
233
- def send(self, peer:bytes|str, msg:str) -> dict:
269
+ def send(self, peer: bytes | str, msg: str) -> dict:
234
270
  """
235
271
  Send a command over ESPNow.
236
272
  :param peer: Binary MAC address of another device.
@@ -246,9 +282,11 @@ class ESPNowSS:
246
282
  peer = _peer
247
283
 
248
284
  peer_name = hexlify(peer, ':').decode() if peer_name is None else peer_name
249
- 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()
250
288
  # Create an asynchronous sending task.
251
- return NativeTask().create(callback=self._asend_task(peer, task_id, msg), tag=task_id)
289
+ return NativeTask().create(callback=self._asend_task(tid, peer, task_id, msg), tag=task_id)
252
290
 
253
291
  def cluster_send(self, msg):
254
292
  """
@@ -262,14 +300,14 @@ class ESPNowSS:
262
300
  # ----------- OTHER METHODS --------------
263
301
  def save_peers(self):
264
302
  try:
265
- with open(self.peer_cache_path, "w") as f:
303
+ with open(self.peer_cache_path, "w", encoding="utf-8") as f:
266
304
  dump([[list(k), v] for k, v in self.devices.items()], f)
267
305
  return True
268
306
  except Exception as e:
269
307
  syslog(f"[ERR][ESPNOW] Saving peers: {e}")
270
308
  return False
271
309
 
272
- async def _handshake(self, peer:bytes, tag:str):
310
+ async def _handshake(self, peer: bytes, tag: str):
273
311
  """
274
312
  Handshake with peer
275
313
  - with device caching
@@ -295,16 +333,34 @@ class ESPNowSS:
295
333
  my_task.out = f"Handshake: {result} from {self.devices.get(peer)} [{'OK' if is_ok else 'NOK'}]"
296
334
  sender_task.cancel() # Delete sender task (cleanup)
297
335
 
298
- def handshake(self, peer:bytes|str):
299
- 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"
300
356
  # Create an asynchronous sending task.
301
357
  if isinstance(peer, str) and ":" in peer:
302
- # Convert 50:02:91:86:34:28 format to b'P\x02\x91\x864(' bytes
303
- 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
304
361
  if isinstance(peer, bytes):
305
362
  return NativeTask().create(callback=self._handshake(peer, task_id), tag=task_id)
306
- else:
307
- return {None: "Invalid MAC address format. Use 50:02:91:86:34:28 or b'P\\x02\\x91\\x864('"}
363
+ return {None: "Invalid MAC address format. Use 50:02:91:86:34:28 or b'P\\x02\\x91\\x864('"}
308
364
 
309
365
  def stats(self):
310
366
  """
@@ -321,11 +377,14 @@ class ESPNowSS:
321
377
  except Exception as e:
322
378
  _peers = str(e)
323
379
  return {"stats": _stats, "peers": _peers, "ready": self.server_ready}
324
-
380
+
325
381
  def members(self):
382
+ """
383
+ Returns the list of devices that are members of the current group.
384
+ """
326
385
  return self.devices
327
386
 
328
- def remove_peer(self, peer:bytes) -> bool:
387
+ def remove_peer(self, peer: bytes) -> bool:
329
388
  """
330
389
  Remove peer from ESPNow devices
331
390
  :param peer: MAC address as bytes to remove
micrOS/source/Files.py CHANGED
@@ -3,41 +3,49 @@ Module is responsible high level micropython file system opeartions
3
3
  [IMPORTANT] This module must never use any micrOS specific functions or classes.
4
4
  """
5
5
 
6
- from uos import ilistdir, remove, stat, getcwd, mkdir
6
+ from uos import ilistdir, remove, stat, getcwd, mkdir, rmdir
7
7
  from sys import path as upath
8
8
 
9
9
  ################################ Helper functions #####################################
10
10
 
11
11
  def _filter(path:str='/', ext:tuple=None, prefix:tuple=None, hide_core:bool=True) -> bool:
12
12
  """
13
- Filter files
14
- :param path: file to check
13
+ Filter files in the micrOS filesystem.
14
+
15
+ :param path: file path to check
15
16
  :param ext: tuple of extensions to filter by, default: None (all)
16
- :param hide_core: hide core files (mpy, py), default: True
17
+ :param prefix: tuple of prefixes to match (e.g. ('LM', 'IO')), default: None
18
+ :param hide_core: if True, hides core .py/.mpy files in the root (current) directory
19
+ :return: bool, whether the file passes the filter
17
20
  """
21
+ parent = "/".join(path.split("/")[:-1]) or "/"
18
22
  fname = path.split("/")[-1]
19
23
  _ext = fname.split(".")[-1]
20
- if hide_core and _ext in ("mpy", "py") and not (fname.startswith("LM_") or fname.startswith("IO_")):
24
+
25
+ # --- Hide core logic ---
26
+ # Core = any .py/.mpy in the current (root) working directory
27
+ if hide_core and _ext in ("mpy", "py") and parent in ('/', ""):
21
28
  return False
29
+
30
+ # --- General matching rules ---
22
31
  if ext is None and prefix is None:
23
32
  return True
24
33
  if isinstance(prefix, tuple) and fname.split("_")[0] in prefix:
25
34
  return True
26
- if isinstance(ext, tuple) and fname.split(".")[-1] in ext:
35
+ if isinstance(ext, tuple) and _ext in ext:
27
36
  return True
28
37
  return False
29
38
 
30
39
  def is_protected(path:str='/') -> bool:
31
40
  """
32
- Check is file protected
33
- - deny deletion
41
+ Check is file/dir protected
42
+ - every file and folder is protected in root dir: /
43
+ - with protected file list
34
44
  """
35
- protected_entities = ("", "node_config.json", "modules", "config", "logs", "web", "data",
36
- "LM_pacman.mpy", "LM_system.mpy")
37
- entity = path.split("/")[-1].replace("/", "")
38
- if entity in protected_entities:
39
- return True
40
- return False
45
+ protected_files = ("node_config.json", "LM_system.mpy", "LM_pacman.mpy")
46
+ parent = "/".join(path.split("/")[:-1]) or "/"
47
+ fname = path.split("/")[-1]
48
+ return parent in ("/", "") or fname in protected_files
41
49
 
42
50
  def _type_mask_to_str(item_type:int=None) -> str:
43
51
  # Map the raw bit-mask to a single character
@@ -83,7 +91,7 @@ def ilist_fs(path:str="/", type_filter:str='*', select:str='*', core:bool=False)
83
91
  if type_filter in ("*", item_type):
84
92
  # Mods only
85
93
  _select = None if select == "*" else (select,)
86
- if item_type == 'f' and not _filter(item, prefix=_select, hide_core=not core):
94
+ if item_type == 'f' and not _filter(path_join(path, item), prefix=_select, hide_core=not core):
87
95
  continue
88
96
  if select != '*' and item_type == 'd':
89
97
  continue
@@ -102,20 +110,38 @@ def list_fs(path:str="/", type_filter:str='*', select:str='*', core:bool=False)
102
110
  return list(ilist_fs(path, type_filter, select, core))
103
111
 
104
112
 
105
- def remove_fs(path, allow_dir=False):
113
+ def remove_file(path, force=False):
106
114
  """
107
115
  Linux like rm command - delete app resources and folders
108
- :param path: app resource path
109
- :param allow_dir: enable directory deletion, default: False
116
+ :param path: file to delete
117
+ :param force: pypass file protection check - sudo mode
110
118
  """
111
119
  # protect some resources
112
- if is_protected(path):
113
- return f'{path} is protected, skip deletion'
114
- _is_file = is_file(path)
115
- if _is_file or allow_dir:
120
+ if not force and is_protected(path):
121
+ return f'Protected resource, skip deletion: {path}'
122
+ if is_file(path):
116
123
  remove(path)
117
124
  return f"{path} deleted"
118
- return f"Cannot delete {'file' if _is_file else 'dir'}: {path}"
125
+ return f"Cannot delete dir type: {path}"
126
+
127
+
128
+ def remove_dir(path, force=False):
129
+ """
130
+ Recursively delete a folder and all its contents.
131
+ :param path: folder to delete
132
+ :param force: pypass dir protection check - sudo mode
133
+ """
134
+ # protect some resources
135
+ if not force and is_protected(path):
136
+ return f'Protected resource, skip deletion: {path}'
137
+ for entry in ilistdir(path):
138
+ content_path = path_join(path, entry[0])
139
+ if is_dir(content_path): # directory flag
140
+ remove_dir(content_path)
141
+ else:
142
+ remove(content_path)
143
+ rmdir(path)
144
+ return f"{path} deleted"
119
145
 
120
146
 
121
147
  def path_join(*parts):
@@ -133,6 +159,7 @@ class OSPath:
133
159
  WEB = path_join(_ROOT,'/web') # Web resources (.html, .css, .js, .json, etc.)
134
160
  MODULES = path_join(_ROOT, '/modules') # Application modules (.mpy, .py)
135
161
  CONFIG = path_join(_ROOT, '/config') # System configuration files (node_config.json, etc.)
162
+ LIB = path_join(_ROOT, '/lib') # Official and Custom package installation target path
136
163
 
137
164
 
138
165
  def init_micros_dirs():
@@ -13,6 +13,7 @@ Designed by
13
13
  from socket import getaddrinfo, SOCK_STREAM
14
14
  from re import compile as re_compile
15
15
  from json import loads
16
+ from binascii import hexlify
16
17
  from uasyncio import open_connection
17
18
 
18
19
  from Debug import syslog
@@ -170,7 +171,7 @@ class InterCon:
170
171
  """
171
172
  response = await self.send_cmd(host, ["task", "list", ">json"])
172
173
  if not response:
173
- return {None: f"[ERR] ESPNow auto handshake: task list, {response}"}
174
+ return {None: f"[ERR] ESPNow auto handshake: task list >>{host}: {response}"}
174
175
 
175
176
  active_tasks = loads(response).get("active")
176
177
  if "espnow.server" in active_tasks:
@@ -254,7 +255,8 @@ def send_cmd(host:str, cmd:list|str) -> dict:
254
255
  _mod = cmd[0]
255
256
  if InterCon.validate_ipv4(host):
256
257
  return f"{'.'.join(host.split('.')[-2:])}.{_mod}"
257
- return f"{host.replace('.local', '')}.{_mod}"
258
+ target = ".".join(host.split(".")[0:-1]) if "." in host else host
259
+ return f"{target}.{_mod}"
258
260
 
259
261
  com_obj = InterCon()
260
262
  task_id = f"con.{_tagify()}" # CHECK TASK ID CONFLICT
@@ -265,4 +267,8 @@ def host_cache() -> dict:
265
267
  """
266
268
  Dump InterCon connection cache
267
269
  """
268
- return InterCon.CONN_MAP
270
+ if ESPNowSS is None:
271
+ return InterCon.CONN_MAP
272
+ all_devs = dict({name:hexlify(mac, ':').decode() for mac, name in ESPNowSS().devices.items()})
273
+ all_devs.update(InterCon.CONN_MAP)
274
+ return all_devs