micrOSDevToolKit 2.10.2__py3-none-any.whl → 2.10.5__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.
- micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +26 -18
- micrOS/source/Common.py +2 -2
- micrOS/source/Espnow.py +245 -123
- micrOS/source/Files.py +101 -0
- micrOS/source/LM_espnow.py +10 -7
- micrOS/source/LM_mqtt_pro.py +211 -0
- micrOS/source/LM_pacman.py +37 -57
- micrOS/source/LM_system.py +1 -1
- micrOS/source/Logger.py +5 -11
- micrOS/source/Shell.py +23 -19
- micrOS/source/Tasks.py +2 -2
- micrOS/source/__pycache__/Common.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/Logger.cpython-312.pyc +0 -0
- micrOS/source/microIO.py +3 -2
- micrOS/source/urequests.py +10 -1
- {micrOSDevToolKit-2.10.2.dist-info → microsdevtoolkit-2.10.5.dist-info}/METADATA +3 -2
- {micrOSDevToolKit-2.10.2.dist-info → microsdevtoolkit-2.10.5.dist-info}/RECORD +34 -30
- {micrOSDevToolKit-2.10.2.dist-info → microsdevtoolkit-2.10.5.dist-info}/WHEEL +1 -1
- toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
- toolkit/simulator_lib/uos.py +5 -5
- toolkit/workspace/precompiled/Espnow.mpy +0 -0
- toolkit/workspace/precompiled/Files.mpy +0 -0
- toolkit/workspace/precompiled/LM_espnow.py +10 -7
- toolkit/workspace/precompiled/LM_mqtt_pro.py +211 -0
- toolkit/workspace/precompiled/LM_pacman.mpy +0 -0
- toolkit/workspace/precompiled/LM_system.mpy +0 -0
- toolkit/workspace/precompiled/Logger.mpy +0 -0
- toolkit/workspace/precompiled/Shell.mpy +0 -0
- toolkit/workspace/precompiled/Tasks.mpy +0 -0
- toolkit/workspace/precompiled/microIO.mpy +0 -0
- toolkit/workspace/precompiled/urequests.mpy +0 -0
- {micrOSDevToolKit-2.10.2.data → microsdevtoolkit-2.10.5.data}/scripts/devToolKit.py +0 -0
- {micrOSDevToolKit-2.10.2.dist-info → microsdevtoolkit-2.10.5.dist-info/licenses}/LICENSE +0 -0
- {micrOSDevToolKit-2.10.2.dist-info → microsdevtoolkit-2.10.5.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
|
-
|
|
33
|
+
18
|
|
30
34
|
],
|
|
31
35
|
"reset.py": [
|
|
32
36
|
10.0,
|
|
33
37
|
0
|
|
34
38
|
],
|
|
35
39
|
"Shell.py": [
|
|
36
|
-
9.
|
|
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.
|
|
52
|
+
8.9,
|
|
49
53
|
4
|
|
50
54
|
],
|
|
51
55
|
"Common.py": [
|
|
52
56
|
9.78,
|
|
53
|
-
|
|
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.
|
|
72
|
+
9.49,
|
|
69
73
|
1
|
|
70
74
|
],
|
|
71
75
|
"Scheduler.py": [
|
|
@@ -73,7 +77,7 @@
|
|
|
73
77
|
1
|
|
74
78
|
],
|
|
75
79
|
"microIO.py": [
|
|
76
|
-
9.
|
|
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.
|
|
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.
|
|
108
|
+
8.92,
|
|
105
109
|
0
|
|
106
110
|
],
|
|
107
111
|
"LM_genIO.py": [
|
|
@@ -113,7 +117,7 @@
|
|
|
113
117
|
0
|
|
114
118
|
],
|
|
115
119
|
"LM_system.py": [
|
|
116
|
-
8.
|
|
120
|
+
8.28,
|
|
117
121
|
5
|
|
118
122
|
],
|
|
119
123
|
"LM_robustness.py": [
|
|
@@ -237,7 +241,7 @@
|
|
|
237
241
|
0
|
|
238
242
|
],
|
|
239
243
|
"LM_espnow.py": [
|
|
240
|
-
7.
|
|
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
|
-
|
|
319
|
-
|
|
326
|
+
3550,
|
|
327
|
+
24
|
|
320
328
|
],
|
|
321
329
|
"load": [
|
|
322
|
-
|
|
323
|
-
|
|
330
|
+
9217,
|
|
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
|
-
|
|
339
|
+
6
|
|
332
340
|
],
|
|
333
|
-
"core_score": 9.
|
|
334
|
-
"load_score": 8.
|
|
335
|
-
"version": "2.10.
|
|
341
|
+
"core_score": 9.24,
|
|
342
|
+
"load_score": 8.23,
|
|
343
|
+
"version": "2.10.5-0"
|
|
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
|
-
|
|
10
|
+
# ----------- PARSE AND RENDER MSG PROTOCOL --------------
|
|
11
|
+
|
|
12
|
+
def render_response(tid, oper, data, prompt) -> str:
|
|
13
13
|
"""
|
|
14
|
-
|
|
15
|
-
The prompt indicates the end-of-message with a '$' marker.
|
|
14
|
+
Render ESPNow custom message (protocol)
|
|
16
15
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
25
|
+
def parse_request(msg:bytes) -> (bool, dict|str):
|
|
23
26
|
"""
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
52
|
-
except
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
80
|
+
|
|
81
|
+
class ESPNowSS:
|
|
60
82
|
"""
|
|
61
|
-
ESPNow
|
|
83
|
+
ESPNow Session Server
|
|
62
84
|
"""
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
158
|
+
await self.__asend_raw(mac, response)
|
|
78
159
|
else:
|
|
79
160
|
errlog_add(response)
|
|
80
|
-
|
|
81
|
-
#
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|