micrOSDevToolKit 2.10.2__py3-none-any.whl → 2.10.6__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 (44) hide show
  1. micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +28 -20
  2. micrOS/source/Common.py +2 -2
  3. micrOS/source/Espnow.py +245 -123
  4. micrOS/source/Files.py +101 -0
  5. micrOS/source/LM_espnow.py +10 -7
  6. micrOS/source/LM_mqtt_pro.py +211 -0
  7. micrOS/source/LM_oled_ui.py +18 -5
  8. micrOS/source/LM_pacman.py +37 -57
  9. micrOS/source/LM_rest.py +3 -3
  10. micrOS/source/LM_system.py +1 -1
  11. micrOS/source/Logger.py +5 -11
  12. micrOS/source/Shell.py +33 -28
  13. micrOS/source/Tasks.py +2 -2
  14. micrOS/source/__pycache__/Common.cpython-312.pyc +0 -0
  15. micrOS/source/__pycache__/Logger.cpython-312.pyc +0 -0
  16. micrOS/source/microIO.py +3 -2
  17. micrOS/source/urequests.py +10 -1
  18. {micrOSDevToolKit-2.10.2.dist-info → microsdevtoolkit-2.10.6.dist-info}/METADATA +3 -2
  19. {micrOSDevToolKit-2.10.2.dist-info → microsdevtoolkit-2.10.6.dist-info}/RECORD +44 -40
  20. {micrOSDevToolKit-2.10.2.dist-info → microsdevtoolkit-2.10.6.dist-info}/WHEEL +1 -1
  21. toolkit/Gateway.py +3 -3
  22. toolkit/simulator_lib/__pycache__/machine.cpython-312.pyc +0 -0
  23. toolkit/simulator_lib/__pycache__/sim_console.cpython-312.pyc +0 -0
  24. toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
  25. toolkit/simulator_lib/uos.py +5 -5
  26. toolkit/user_data/webhooks/generic.py +1 -1
  27. toolkit/user_data/webhooks/macro.py +1 -1
  28. toolkit/user_data/webhooks/template.py +1 -1
  29. toolkit/workspace/precompiled/Espnow.mpy +0 -0
  30. toolkit/workspace/precompiled/Files.mpy +0 -0
  31. toolkit/workspace/precompiled/LM_espnow.py +10 -7
  32. toolkit/workspace/precompiled/LM_mqtt_pro.py +211 -0
  33. toolkit/workspace/precompiled/LM_oled_ui.mpy +0 -0
  34. toolkit/workspace/precompiled/LM_pacman.mpy +0 -0
  35. toolkit/workspace/precompiled/LM_rest.mpy +0 -0
  36. toolkit/workspace/precompiled/LM_system.mpy +0 -0
  37. toolkit/workspace/precompiled/Logger.mpy +0 -0
  38. toolkit/workspace/precompiled/Shell.mpy +0 -0
  39. toolkit/workspace/precompiled/Tasks.mpy +0 -0
  40. toolkit/workspace/precompiled/microIO.mpy +0 -0
  41. toolkit/workspace/precompiled/urequests.mpy +0 -0
  42. {micrOSDevToolKit-2.10.2.data → microsdevtoolkit-2.10.6.data}/scripts/devToolKit.py +0 -0
  43. {micrOSDevToolKit-2.10.2.dist-info → microsdevtoolkit-2.10.6.dist-info/licenses}/LICENSE +0 -0
  44. {micrOSDevToolKit-2.10.2.dist-info → microsdevtoolkit-2.10.6.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,10 @@
4
4
  9.16,
5
5
  7
6
6
  ],
7
+ "Files.py": [
8
+ 9.6,
9
+ 4
10
+ ],
7
11
  "micrOSloader.py": [
8
12
  7.33,
9
13
  1
@@ -26,14 +30,14 @@
26
30
  ],
27
31
  "Config.py": [
28
32
  9.48,
29
- 17
33
+ 18
30
34
  ],
31
35
  "reset.py": [
32
36
  10.0,
33
37
  0
34
38
  ],
35
39
  "Shell.py": [
36
- 9.34,
40
+ 9.36,
37
41
  1
38
42
  ],
39
43
  "Notify.py": [
@@ -45,12 +49,12 @@
45
49
  29
46
50
  ],
47
51
  "Logger.py": [
48
- 8.85,
52
+ 8.9,
49
53
  4
50
54
  ],
51
55
  "Common.py": [
52
56
  9.78,
53
- 34
57
+ 35
54
58
  ],
55
59
  "InterConnect.py": [
56
60
  9.41,
@@ -65,7 +69,7 @@
65
69
  8
66
70
  ],
67
71
  "Espnow.py": [
68
- 9.43,
72
+ 9.5,
69
73
  1
70
74
  ],
71
75
  "Scheduler.py": [
@@ -73,7 +77,7 @@
73
77
  1
74
78
  ],
75
79
  "microIO.py": [
76
- 9.42,
80
+ 9.43,
77
81
  42
78
82
  ],
79
83
  "micrOS.py": [
@@ -89,7 +93,7 @@
89
93
  0
90
94
  ],
91
95
  "urequests.py": [
92
- 8.99,
96
+ 8.96,
93
97
  5
94
98
  ],
95
99
  "LM_roboarm.py": [
@@ -101,7 +105,7 @@
101
105
  1
102
106
  ],
103
107
  "LM_pacman.py": [
104
- 8.94,
108
+ 8.92,
105
109
  0
106
110
  ],
107
111
  "LM_genIO.py": [
@@ -109,11 +113,11 @@
109
113
  0
110
114
  ],
111
115
  "LM_oled_ui.py": [
112
- 8.64,
116
+ 8.65,
113
117
  0
114
118
  ],
115
119
  "LM_system.py": [
116
- 8.19,
120
+ 8.28,
117
121
  5
118
122
  ],
119
123
  "LM_robustness.py": [
@@ -125,7 +129,7 @@
125
129
  0
126
130
  ],
127
131
  "LM_rest.py": [
128
- 7.95,
132
+ 8.18,
129
133
  0
130
134
  ],
131
135
  "LM_oled.py": [
@@ -237,7 +241,7 @@
237
241
  0
238
242
  ],
239
243
  "LM_espnow.py": [
240
- 7.5,
244
+ 7.89,
241
245
  0
242
246
  ],
243
247
  "LM_telegram.py": [
@@ -272,6 +276,10 @@
272
276
  8.9,
273
277
  4
274
278
  ],
279
+ "LM_mqtt_pro.py": [
280
+ 9.35,
281
+ 0
282
+ ],
275
283
  "LM_trackball.py": [
276
284
  8.31,
277
285
  1
@@ -315,12 +323,12 @@
315
323
  },
316
324
  "summary": {
317
325
  "core": [
318
- 3380,
319
- 23
326
+ 3554,
327
+ 24
320
328
  ],
321
329
  "load": [
322
- 9077,
323
- 55
330
+ 9227,
331
+ 56
324
332
  ],
325
333
  "core_dep": [
326
334
  true,
@@ -328,10 +336,10 @@
328
336
  ],
329
337
  "load_dep": [
330
338
  true,
331
- 5
339
+ 6
332
340
  ],
333
- "core_score": 9.22,
334
- "load_score": 8.2,
335
- "version": "2.10.2-0"
341
+ "core_score": 9.24,
342
+ "load_score": 8.23,
343
+ "version": "2.10.5-1"
336
344
  }
337
345
  }
micrOS/source/Common.py CHANGED
@@ -125,10 +125,10 @@ def manage_task(tag, operation):
125
125
  raise Exception(f"Invalid operation: {operation}")
126
126
 
127
127
 
128
- def exec_cmd(cmd, jsonify:bool=None, skip_check=False):
128
+ def exec_cmd(cmd:list, jsonify:bool=None, skip_check=False):
129
129
  """
130
130
  [LM] Single (sync) LM execution
131
- :param cmd: command string list
131
+ :param cmd: command string list, ex.: ['system', 'clock']
132
132
  :param jsonify: request json output
133
133
  :param skip_check: skip cmd type check, micropython bug
134
134
  return state, output
micrOS/source/Espnow.py CHANGED
@@ -1,157 +1,279 @@
1
1
  from aioespnow import AIOESPNow
2
2
  from binascii import hexlify
3
3
  from Tasks import NativeTask, TaskBase, lm_exec, lm_is_loaded
4
+ import uasyncio as asyncio
4
5
  from Network import get_mac
5
6
  from Config import cfgget
6
7
  from Debug import errlog_add
7
8
 
8
- # Configuration values and globals
9
- _INSTANCE = None
10
- _DEVFID = cfgget('devfid') # for example, "node01"
11
9
 
12
- async def asend(now, mac, msg):
10
+ # ----------- PARSE AND RENDER MSG PROTOCOL --------------
11
+
12
+ def render_response(tid, oper, data, prompt) -> str:
13
13
  """
14
- Send a message over ESPNow. The sent message will be extended with the server prompt.
15
- The prompt indicates the end-of-message with a '$' marker.
14
+ Render ESPNow custom message (protocol)
16
15
  """
17
- prompt = f"{_DEVFID}$" # '$' symbol is the ACK (end of message)!
18
- # Append a newline and the server prompt to the message.
19
- full_msg = f"{msg}\n{prompt}".encode("utf-8")
20
- return await now.asend(mac, full_msg)
16
+ if oper not in ("REQ", "RSP"):
17
+ errlog_add(f"[ERR] espnow render_response, unknown oper: {oper}")
18
+ tmp = "{tid}|{oper}|{data}|{prompt}$"
19
+ tmp = tmp.replace("{tid}", str(tid))
20
+ tmp = tmp.replace("{oper}", str(oper))
21
+ tmp = tmp.replace("{data}", str(data))
22
+ tmp = tmp.replace("{prompt}", str(prompt))
23
+ return tmp
21
24
 
22
- def _serv_execute(msg, my_task):
25
+ def parse_request(msg:bytes) -> (bool, dict|str):
23
26
  """
24
- Process an incoming command and return a tuple (ready, response).
25
-
26
- The function decodes the message, strips any trailing '$' (which marks the prompt),
27
- and then interprets the message as a command. The command is split into tokens where
28
- the first token is considered the module/command. If the module is allowed (lm_is_loaded),
29
- the command is executed with lm_exec; otherwise, "NotAllowed" is returned.
27
+ Parse ESPNow custom message protocol
30
28
  """
31
29
  try:
32
- command_line = msg.decode('utf-8').strip()
30
+ msg = msg.decode('utf-8').strip()
33
31
  except UnicodeError:
34
- my_task.out = "[NOW SERVE] Invalid encoding"
35
32
  return False, "Invalid encoding"
33
+ # strip the trailing '$' then split on '|'
34
+ parts = msg.rstrip("$").split("|")
35
+ if len(parts) == 4:
36
+ return True, {"tid": parts[0],
37
+ "oper": parts[1],
38
+ "data": parts[2],
39
+ "prompt": parts[3]}
40
+ return False, f"Missing 4 args: {msg}"
41
+
42
+
43
+ # ----------- ESPNOW SESSION SERVER - LISTENER AND SENDER --------------
44
+ class ResponseRouter:
45
+ """
46
+ Response Router (by mac address)
47
+ to connect sender task with receiver loop (aka server)
48
+ """
49
+ _routes: dict[bytes, "ResponseRouter"] = {}
50
+
51
+ def __init__(self, mac: bytes):
52
+ self.mac = mac
53
+ self.response = None
54
+ self._event = asyncio.Event()
55
+ ResponseRouter._routes[mac] = self
36
56
 
37
- # Split the command into tokens.
38
- tokens = command_line.split()
39
- if not tokens:
40
- return False, "[NOW SERVE] Empty command"
41
- # Remove trailing prompt marker if present.
42
- client_token = "?$"
43
- if tokens[-1].endswith("$"):
44
- client_token = tokens[-1]
45
- tokens = tokens[:-1]
46
- my_task.out = f"[NOW SERVE] {' '.join(tokens)} (from {client_token})"
47
- # Check if the module/command is allowed.
48
- module = tokens[0]
49
- if lm_is_loaded(module):
57
+ async def get_response(self, timeout:int=10) -> str|dict:
58
+ """Wait for one response, then clear the event for reuse."""
50
59
  try:
51
- state, out = lm_exec(tokens)
52
- except Exception as e:
53
- # Optionally log the exception here.
54
- state, out = False, f"[ERR][NOW SERVE] {tokens}: {e}"
55
- else:
56
- state, out = False, f"[WARN][NOW SERVE] NotAllowed {tokens[0]}"
57
- return state, out
60
+ await asyncio.wait_for(self._event.wait(), timeout)
61
+ except asyncio.TimeoutError:
62
+ return "Timeout has beed exceeded"
63
+ self._event.clear()
64
+ return self.response
65
+
66
+ @staticmethod
67
+ def update_response(mac: bytes, response: str) -> None:
68
+ # USE <tid> for proper session response mapping
69
+ router = ResponseRouter._routes.get(mac)
70
+ if router is None:
71
+ errlog_add(f"[WARN][ESPNOW] No response route for {mac}")
72
+ return
73
+ router.response = response
74
+ router._event.set()
75
+
76
+ def close(self) -> None:
77
+ """Remove routing entry when done."""
78
+ ResponseRouter._routes.pop(self.mac, None)
58
79
 
59
- async def _server(now):
80
+
81
+ class ESPNowSS:
60
82
  """
61
- ESPNow server task that continuously waits for incoming messages and processes commands.
83
+ ESPNow Session Server
62
84
  """
63
- with TaskBase.TASKS.get('espnow.server', None) as my_task:
64
- async for mac, msg in now:
65
- try:
66
- state, response = _serv_execute(msg, my_task)
67
- if state:
68
- await asend(now, mac, response)
69
- else:
70
- errlog_add(response)
71
- except OSError as err:
72
- # If the peer is not yet added, add it and retry.
73
- if len(err.args) > 1 and err.args[1] == 'ESP_ERR_ESPNOW_NOT_FOUND':
74
- now.add_peer(mac)
75
- state, response = _serv_execute(msg, my_task)
85
+ _instance = None
86
+
87
+ def __new__(cls, *args, **kwargs):
88
+ if cls._instance is None:
89
+ # first time: actually create it
90
+ cls._instance = super().__new__(cls)
91
+ return cls._instance
92
+
93
+ def __init__(self):
94
+ # __init__ still runs each time, so guard if needed
95
+ if not hasattr(self, '_initialized'):
96
+ self.espnow = AIOESPNow() # Instance with async support
97
+ self.espnow.active(True)
98
+ self.devfid = cfgget('devfid')
99
+ self.devices: dict[bytes, str] = {} # mapping for { "mac address": "devfid" } pairs
100
+ self._initialized = True
101
+ self.server_ready = False
102
+
103
+ # ----------- SERVER METHODS --------------
104
+ def _request_handler(self, msg:bytes, my_task:NativeTask, mac:bytes):
105
+ """
106
+ Handle server input message (request), with REQ/RSP types (oper)
107
+ oper==REQ - command execution
108
+ oper==RSP - command response
109
+ :param msg: valid message format> "{tid}|{oper}|{data}|{prompt}$", {data} is the user input
110
+ :param my_task: Server task instance, for my_task.out update
111
+ :param mac: sender binary mac address
112
+ """
113
+
114
+ state, request = parse_request(msg)
115
+ if not state:
116
+ my_task.out = f"[_ESPNOW] {request}"
117
+ return state, request
118
+
119
+ # parsed request: {"tid": "n/a", "oper": "n/a", "data": "n/a", "prompt": "n/a"}
120
+ operation, prompt, tid = request["oper"], request["prompt"], request["tid"]
121
+ my_task.out = f"[{tid}] {operation} from {prompt}"
122
+ # Update known devices
123
+ self.devices[mac] = request["prompt"]
124
+
125
+ # Check if the module/command is allowed., check oper==REQ/RSP
126
+ if operation == "REQ":
127
+ command = request["data"].split()
128
+ module = command[0]
129
+ if lm_is_loaded(module):
130
+ try:
131
+ state, out = lm_exec(command)
132
+ # rendered_output: "{tid}|{oper}|{data}|{prompt}$"
133
+ rendered_out = render_response(tid="?", oper="RSP", data=out, prompt=self.devfid)
134
+ return state, rendered_out
135
+ except Exception as e:
136
+ # Optionally log the exception here.
137
+ state, out = False, f"[ERR][_ESPNOW] {command}: {e}"
138
+ else:
139
+ state, out = False, f"[WARN][_ESPNOW] NotAllowed {module}"
140
+ return state, out
141
+ if operation == "RSP":
142
+ resp_data = request["data"]
143
+ ResponseRouter.update_response(mac, resp_data) # USE <tid> for proper session response mapping
144
+ return False, f"[_ESPNOW] No action, {request}"
145
+
146
+ async def _server(self, tag:str):
147
+ """
148
+ ESPnow async listener task
149
+ :param tag: micro_task tag for task access
150
+ """
151
+ with TaskBase.TASKS.get(tag, None) as my_task:
152
+ self.server_ready = True
153
+ my_task.out = "ESPNow receiver running"
154
+ async for mac, msg in self.espnow:
155
+ try:
156
+ state, response = self._request_handler(msg, my_task, mac)
76
157
  if state:
77
- await asend(now, mac, response)
158
+ await self.__asend_raw(mac, response)
78
159
  else:
79
160
  errlog_add(response)
80
- else:
81
- # Optionally handle or log other OSErrors here.
82
- errlog_add(f"[ERR][NOW SERVER] {err}")
161
+ except OSError as err:
162
+ # If the peer is not yet added, add it and retry.
163
+ if len(err.args) > 1 and err.args[1] == 'ESP_ERR_ESPNOW_NOT_FOUND':
164
+ self.add_peer(mac)
165
+ state, response = self._request_handler(msg, my_task, mac)
166
+ if state:
167
+ await self.__asend_raw(mac, response)
168
+ else:
169
+ errlog_add(response)
170
+ else:
171
+ # Optionally handle or log other OSErrors here.
172
+ errlog_add(f"[ERR][NOW SERVER] {err}")
173
+
174
+ def start_server(self):
175
+ """
176
+ Start the async ESPNow receiver server.
177
+ """
178
+ # Create an asynchronous task with tag 'espnow.server'
179
+ tag = 'espnow.server'
180
+ state = NativeTask().create(callback=self._server(tag), tag=tag)
181
+ return "Starting" if state else "Already running"
182
+
183
+ #----------- SEND METHODS --------------
184
+ async def __asend_raw(self, mac:bytes, msg:str):
185
+ """
186
+ ESPnow send message to mac address
187
+ """
188
+ return await self.espnow.asend(mac, msg.encode("utf-8"))
189
+
190
+ async def _asend_task(self, peer:bytes, tag:str, msg:str):
191
+ """
192
+ ESPNow client task: send a command to a peer and update task status.
193
+ """
194
+ with TaskBase.TASKS.get(tag, None) as my_task:
195
+ router = ResponseRouter(peer)
196
+ # rendered_output: "{tid}|{oper}|{data}|{prompt}$"
197
+ rendered_out = render_response(tid="?", oper="REQ", data=msg, prompt=self.devfid)
198
+ if await self.__asend_raw(peer, rendered_out):
199
+ my_task.out = f"[ESPNOW SEND] {rendered_out}"
200
+ my_task.out = await router.get_response()
201
+ else:
202
+ my_task.out = "[ESPNOW SEND] Peer not responding"
203
+ router.close()
204
+
205
+ def mac_by_peer_name(self, peer_name:str) -> bytes|None:
206
+ matches = [k for k, v in self.devices.items() if v == peer_name]
207
+ peer = matches[0] if matches else None
208
+ return peer
209
+
210
+ def send(self, peer:bytes|str, msg:str) -> str:
211
+ """
212
+ Send a command over ESPNow.
213
+ :param peer: Binary MAC address of another device.
214
+ :param msg: String command message to send.
215
+ """
216
+ peer_name = None
217
+ if isinstance(peer, str):
218
+ # Peer as device name (prompt)
219
+ _peer = self.mac_by_peer_name(peer)
220
+ if _peer is None:
221
+ return f"Unknown device: {peer}"
222
+ peer_name = peer
223
+ peer = _peer
224
+
225
+ peer_name = hexlify(peer, ':').decode() if peer_name is None else peer_name
226
+ task_id = f"con.espnow.{peer_name}"
227
+ # Create an asynchronous sending task.
228
+ state = NativeTask().create(callback=INSTANCE._asend_task(peer, task_id, msg), tag=task_id)
229
+ return "Starting" if state else "Already running"
230
+
231
+ # ----------- OTHER METHODS --------------
232
+
233
+ def add_peer(self, peer:bytes, devfid:str=None):
234
+ """
235
+ Add a peer given its MAC address.
236
+ :param peer: Binary MAC address of a peer (e.g. b'\xbb\xbb\xbb\xbb\xbb\xbb').
237
+ :param devfid: optional parameter to register dev uid for mac address
238
+ """
239
+ try:
240
+ self.espnow.add_peer(peer)
241
+ if devfid is not None:
242
+ # Update known devices
243
+ self.devices[peer] = devfid
244
+ except Exception as e:
245
+ return f"Peer error: {e}"
246
+ return "Peer register done"
247
+
248
+ def stats(self):
249
+ """
250
+ Return stats for ESPNow peers.
251
+ stats: tx_pkts, tx_responses, tx_failures, rx_packets, rx_dropped_packets.
252
+ peers: peer, rssi, time_ms.
253
+ """
254
+ try:
255
+ _stats = self.espnow.stats()
256
+ except Exception as e:
257
+ _stats = str(e)
258
+ try:
259
+ _peers = self.espnow.peers_table
260
+ except Exception as e:
261
+ _peers = str(e)
262
+ return {"stats": _stats, "peers": _peers, "map": self.devices, "ready": self.server_ready}
83
263
 
84
- async def _send(now, peer, tag, msg):
85
- """
86
- ESPNow client task: send a command to a peer and update task status.
87
- """
88
- with TaskBase.TASKS.get(tag, None) as my_task:
89
- if not await asend(now, peer, msg):
90
- my_task.out = "[NOW SEND] Peer not responding"
91
- else:
92
- my_task.out = f"[NOW SEND] {msg}"
93
264
 
94
265
  ###################################################
95
266
  # Control functions #
96
267
  ###################################################
97
- def initialize():
98
- """
99
- Initialize the ESPNow protocol. (WLAN must be active.)
100
- """
101
- global _INSTANCE
102
- if _INSTANCE is None:
103
- now = AIOESPNow() # Instance with async support
104
- now.active(True)
105
- _INSTANCE = now
106
- return _INSTANCE
107
-
108
- def add_peer(now, peer):
109
- """
110
- Add a peer given its MAC address.
111
- :param now: ESPNow instance.
112
- :param peer: Binary MAC address of a peer (e.g. b'\xbb\xbb\xbb\xbb\xbb\xbb').
113
- """
114
- now.add_peer(peer)
115
- return "Peer register done"
268
+ INSTANCE = ESPNowSS()
116
269
 
117
- def espnow_server():
118
- """
119
- Start the async ESPNow receiver server.
120
- """
121
- now = initialize()
122
- # Create an asynchronous task with tag 'espnow.server'
123
- state = NativeTask().create(callback=_server(now), tag='espnow.server')
124
- return "Starting" if state else "Already running"
270
+ def initialize():
271
+ # TODO: remove, use ESPNowSS() class instead
272
+ global INSTANCE
273
+ if INSTANCE is None:
274
+ INSTANCE = ESPNowSS()
275
+ return INSTANCE
125
276
 
126
- def espnow_send(peer, msg):
127
- """
128
- Send a command over ESPNow.
129
- :param peer: Binary MAC address of another device.
130
- :param msg: String command message to send.
131
- """
132
- now = initialize()
133
- mac_str = hexlify(peer, ':').decode()
134
- task_id = f"espnow.cli.{mac_str}"
135
- # Create an asynchronous sending task.
136
- state = NativeTask().create(callback=_send(now, peer, task_id, msg), tag=task_id)
137
- return "Starting" if state else "Already running"
138
-
139
- def stats():
140
- """
141
- Return stats for ESPNow peers.
142
- stats: tx_pkts, tx_responses, tx_failures, rx_packets, rx_dropped_packets.
143
- peers: peer, rssi, time_ms.
144
- """
145
- now = initialize()
146
- try:
147
- _stats = now.stats()
148
- except Exception as e:
149
- _stats = str(e)
150
- try:
151
- _peers = now.peers_table
152
- except Exception as e:
153
- _peers = str(e)
154
- return {"stats": _stats, "peers": _peers}
155
277
 
156
278
  def mac_address():
157
279
  """
micrOS/source/Files.py ADDED
@@ -0,0 +1,101 @@
1
+ from uos import ilistdir, remove, stat
2
+
3
+ ################################ Helper functions #####################################
4
+
5
+ def _is_module(path:str='/', pyprefix:str='*') -> bool:
6
+ """
7
+ Filter application modules, LM_.* (pyprefix) or app data or web resource
8
+ :param path: file to check
9
+ :param pyprefix: python resource filter prefix, default: * (all: LM and IO)
10
+ """
11
+ # micrOS file types
12
+ allowed_exts = ('html', 'js', 'css', 'log', 'pds', 'dat')
13
+ mod_prefixes = ('LM', "IO")
14
+ fname = path.split("/")[-1]
15
+ if fname.split("_")[0] in mod_prefixes or fname.split('.')[-1] in allowed_exts:
16
+ if pyprefix == '*':
17
+ # MODE: ALL app resources
18
+ return True
19
+ if fname.startswith(f"{pyprefix.upper()}_"):
20
+ # MODE: SELECTED app resources
21
+ return True
22
+ return False
23
+
24
+
25
+ def _type_mask_to_str(item_type:int=None) -> str:
26
+ # Map the raw bit-mask to a single character
27
+ if item_type & 0x4000: # Dir bit-mask
28
+ item_type = 'd'
29
+ elif item_type & 0x8000: # File bit-mask
30
+ item_type = 'f'
31
+ else:
32
+ item_type = 'o'
33
+ return item_type
34
+
35
+ ########################### Public functions #############################
36
+
37
+ def ilist_fs(path:str="/", type_filter:str='*', select:str='*', core:bool=False):
38
+ """
39
+ Linux like ls command - list app resources and app folders
40
+ :param path: path to list, default: /
41
+ :param type_filter: content type, default all (*), f-file, d-dir can be selected
42
+ :param select: select specific application resource type by prefix: LM or IO
43
+ :param core: list core files resources as well, default: False
44
+ return iterator:
45
+ when content is all (*) output: [(item_type, item), ...]
46
+ OR
47
+ content type was selected (not *) output: [item, ...]
48
+ """
49
+ path = path if path.endswith('/') else f"{path}/"
50
+ # Info: uos.ilistdir: (name, type, inode[, size])
51
+ for item, item_type, *_ in ilistdir(path):
52
+ item_type = _type_mask_to_str(item_type)
53
+ if type_filter in ("*", item_type):
54
+ # Mods only
55
+ if not core and item_type == 'f' and not _is_module(path+item, pyprefix=select):
56
+ continue
57
+ if select != '*' and item_type == 'd':
58
+ continue
59
+ # Create result
60
+ if type_filter == "*":
61
+ yield item_type, item
62
+ else:
63
+ yield item
64
+
65
+
66
+ def list_fs(path:str="/", type_filter:str='*', select:str='*', core:bool=False) -> list[str,] | list[tuple[str, str],]:
67
+ """
68
+ Wrapper of ilist_fs
69
+ Return list
70
+ """
71
+ return list(ilist_fs(path, type_filter, select, core))
72
+
73
+
74
+ def remove_fs(path, allow_dir=False):
75
+ """
76
+ Linux like rm command - delete app resources and folders
77
+ :param path: app resource path
78
+ :param allow_dir: enable directory deletion, default: False
79
+ """
80
+ # protect some resources
81
+ if 'pacman.mpy' in path or 'system.mpy' in path or "/" == path.strip():
82
+ return f'Load module {path} is protected, skip deletion'
83
+ _is_dir = is_dir(path)
84
+ if _is_module(path) or (_is_dir and allow_dir):
85
+ remove(path)
86
+ return f"Removed: {path} {'dir' if _is_dir else 'file'}"
87
+ return f"Protected path: {path}"
88
+
89
+
90
+ def is_dir(path):
91
+ try:
92
+ return stat(path)[0] & 0x4000
93
+ except OSError:
94
+ return False
95
+
96
+
97
+ def is_file(path):
98
+ try:
99
+ return stat(path)[0] & 0x8000
100
+ except OSError:
101
+ return False