micrOSDevToolKit 2.9.11__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 +37 -29
- micrOS/source/Common.py +5 -5
- micrOS/source/Espnow.py +245 -123
- micrOS/source/Files.py +101 -0
- micrOS/source/InterConnect.py +14 -11
- micrOS/source/LM_espnow.py +10 -7
- micrOS/source/LM_intercon.py +3 -0
- micrOS/source/LM_light_sensor.py +7 -15
- micrOS/source/LM_mqtt_pro.py +211 -0
- micrOS/source/LM_oled_ui.py +18 -22
- micrOS/source/LM_oledui.py +13 -16
- micrOS/source/LM_pacman.py +37 -57
- micrOS/source/LM_presence.py +8 -11
- micrOS/source/LM_system.py +8 -7
- micrOS/source/LM_telegram.py +21 -16
- micrOS/source/Logger.py +5 -11
- micrOS/source/Shell.py +37 -30
- micrOS/source/Tasks.py +32 -17
- micrOS/source/Web.py +3 -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.9.11.dist-info → microsdevtoolkit-2.10.5.dist-info}/METADATA +13 -15
- {micrOSDevToolKit-2.9.11.dist-info → microsdevtoolkit-2.10.5.dist-info}/RECORD +54 -50
- {micrOSDevToolKit-2.9.11.dist-info → microsdevtoolkit-2.10.5.dist-info}/WHEEL +1 -1
- toolkit/dashboard_apps/SystemTest.py +17 -6
- toolkit/lib/micrOSClient.py +25 -6
- toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
- toolkit/simulator_lib/uos.py +5 -5
- toolkit/socketClient.py +2 -3
- toolkit/workspace/precompiled/Common.mpy +0 -0
- toolkit/workspace/precompiled/Espnow.mpy +0 -0
- toolkit/workspace/precompiled/Files.mpy +0 -0
- toolkit/workspace/precompiled/InterConnect.mpy +0 -0
- toolkit/workspace/precompiled/LM_espnow.py +10 -7
- toolkit/workspace/precompiled/LM_intercon.mpy +0 -0
- toolkit/workspace/precompiled/LM_light_sensor.mpy +0 -0
- toolkit/workspace/precompiled/LM_mqtt_pro.py +211 -0
- toolkit/workspace/precompiled/LM_oled_ui.mpy +0 -0
- toolkit/workspace/precompiled/LM_oledui.mpy +0 -0
- toolkit/workspace/precompiled/LM_pacman.mpy +0 -0
- toolkit/workspace/precompiled/LM_presence.mpy +0 -0
- toolkit/workspace/precompiled/LM_system.mpy +0 -0
- toolkit/workspace/precompiled/LM_telegram.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/Web.mpy +0 -0
- toolkit/workspace/precompiled/microIO.mpy +0 -0
- toolkit/workspace/precompiled/urequests.mpy +0 -0
- {micrOSDevToolKit-2.9.11.data → microsdevtoolkit-2.10.5.data}/scripts/devToolKit.py +0 -0
- {micrOSDevToolKit-2.9.11.dist-info → microsdevtoolkit-2.10.5.dist-info/licenses}/LICENSE +0 -0
- {micrOSDevToolKit-2.9.11.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
|
|
@@ -21,23 +25,23 @@
|
|
|
21
25
|
1
|
|
22
26
|
],
|
|
23
27
|
"Tasks.py": [
|
|
24
|
-
9.
|
|
28
|
+
9.38,
|
|
25
29
|
12
|
|
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": [
|
|
40
|
-
|
|
44
|
+
9.74,
|
|
41
45
|
3
|
|
42
46
|
],
|
|
43
47
|
"Types.py": [
|
|
@@ -45,16 +49,16 @@
|
|
|
45
49
|
29
|
|
46
50
|
],
|
|
47
51
|
"Logger.py": [
|
|
48
|
-
8.
|
|
52
|
+
8.9,
|
|
49
53
|
4
|
|
50
54
|
],
|
|
51
55
|
"Common.py": [
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
9.78,
|
|
57
|
+
35
|
|
54
58
|
],
|
|
55
59
|
"InterConnect.py": [
|
|
56
|
-
9.
|
|
57
|
-
|
|
60
|
+
9.41,
|
|
61
|
+
3
|
|
58
62
|
],
|
|
59
63
|
"Debug.py": [
|
|
60
64
|
8.4,
|
|
@@ -65,7 +69,7 @@
|
|
|
65
69
|
8
|
|
66
70
|
],
|
|
67
71
|
"Espnow.py": [
|
|
68
|
-
|
|
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": [
|
|
@@ -109,11 +113,11 @@
|
|
|
109
113
|
0
|
|
110
114
|
],
|
|
111
115
|
"LM_oled_ui.py": [
|
|
112
|
-
|
|
116
|
+
8.64,
|
|
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": [
|
|
@@ -233,15 +237,15 @@
|
|
|
233
237
|
0
|
|
234
238
|
],
|
|
235
239
|
"LM_oledui.py": [
|
|
236
|
-
|
|
240
|
+
7.95,
|
|
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": [
|
|
244
|
-
9.
|
|
248
|
+
9.58,
|
|
245
249
|
0
|
|
246
250
|
],
|
|
247
251
|
"LM_OV2640.py": [
|
|
@@ -261,7 +265,7 @@
|
|
|
261
265
|
0
|
|
262
266
|
],
|
|
263
267
|
"LM_light_sensor.py": [
|
|
264
|
-
|
|
268
|
+
9.17,
|
|
265
269
|
0
|
|
266
270
|
],
|
|
267
271
|
"LM_rp2w.py": [
|
|
@@ -269,9 +273,13 @@
|
|
|
269
273
|
0
|
|
270
274
|
],
|
|
271
275
|
"LM_presence.py": [
|
|
272
|
-
8.
|
|
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
|
|
@@ -298,7 +306,7 @@
|
|
|
298
306
|
],
|
|
299
307
|
"LM_intercon.py": [
|
|
300
308
|
8.18,
|
|
301
|
-
|
|
309
|
+
0
|
|
302
310
|
],
|
|
303
311
|
"LM_ds18.py": [
|
|
304
312
|
6.0,
|
|
@@ -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.
|
|
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
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
micrOS Load Module programming Official API-s
|
|
3
3
|
Designed by Marcell Ban aka BxNxM
|
|
4
4
|
"""
|
|
5
|
-
|
|
6
5
|
from Server import Server, WebCli
|
|
7
6
|
from Debug import errlog_add, console_write
|
|
8
7
|
from Logger import logger, log_get
|
|
@@ -126,18 +125,19 @@ def manage_task(tag, operation):
|
|
|
126
125
|
raise Exception(f"Invalid operation: {operation}")
|
|
127
126
|
|
|
128
127
|
|
|
129
|
-
def exec_cmd(cmd, skip_check=False):
|
|
128
|
+
def exec_cmd(cmd:list, jsonify:bool=None, skip_check=False):
|
|
130
129
|
"""
|
|
131
130
|
[LM] Single (sync) LM execution
|
|
132
|
-
:param cmd: command string list
|
|
131
|
+
:param cmd: command string list, ex.: ['system', 'clock']
|
|
132
|
+
:param jsonify: request json output
|
|
133
133
|
:param skip_check: skip cmd type check, micropython bug
|
|
134
134
|
return state, output
|
|
135
135
|
"""
|
|
136
136
|
# [BUG] Solution with isinstance/type is not reliable... micropython 1.22
|
|
137
137
|
# Invalid type, must be list: <class list>" ...
|
|
138
138
|
if skip_check:
|
|
139
|
-
return lm_exec(cmd)
|
|
140
|
-
return lm_exec(cmd) if isinstance(cmd, list) else False, f"
|
|
139
|
+
return lm_exec(cmd, jsonify=jsonify)
|
|
140
|
+
return lm_exec(cmd, jsonify=jsonify) if isinstance(cmd, list) else False, f"CMD {type(cmd)}, must be list!"
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
def notify(text=None) -> bool:
|
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
|
"""
|