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
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
|
micrOS/source/InterConnect.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from socket import getaddrinfo, SOCK_STREAM
|
|
2
2
|
from re import compile
|
|
3
|
-
|
|
3
|
+
from uasyncio import open_connection
|
|
4
4
|
from Debug import errlog_add
|
|
5
5
|
from Config import cfgget
|
|
6
6
|
from Server import Server
|
|
@@ -22,7 +22,7 @@ class InterCon:
|
|
|
22
22
|
pattern = compile(r'^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$')
|
|
23
23
|
return bool(pattern.match(str_in))
|
|
24
24
|
|
|
25
|
-
async def send_cmd(self, host, cmd):
|
|
25
|
+
async def send_cmd(self, host:str, cmd:list):
|
|
26
26
|
"""
|
|
27
27
|
Async Main method to implement device-device communication with
|
|
28
28
|
- dhcp host resolve and IP caching
|
|
@@ -50,7 +50,7 @@ class InterCon:
|
|
|
50
50
|
if InterCon.validate_ipv4(host):
|
|
51
51
|
try:
|
|
52
52
|
# Create socket object
|
|
53
|
-
self.reader, self.writer = await
|
|
53
|
+
self.reader, self.writer = await open_connection(host, InterCon.PORT)
|
|
54
54
|
# Send command over TCP/IP
|
|
55
55
|
output = await self.__run_command(cmd, hostname)
|
|
56
56
|
except OSError as e:
|
|
@@ -72,14 +72,13 @@ class InterCon:
|
|
|
72
72
|
errlog_add(f"[ERR][intercon] Invalid host: {host}")
|
|
73
73
|
return ''
|
|
74
74
|
|
|
75
|
-
async def __run_command(self, cmd, hostname):
|
|
75
|
+
async def __run_command(self, cmd:list, hostname:str):
|
|
76
76
|
"""
|
|
77
77
|
Implements receive data on open connection, command query and result collection
|
|
78
78
|
:param cmd: command string to server socket shell
|
|
79
79
|
:param hostname: hostname for prompt checking
|
|
80
80
|
Return None here will trigger retry mechanism... + deletes cached IP
|
|
81
81
|
"""
|
|
82
|
-
cmd = str.encode(cmd)
|
|
83
82
|
data, prompt = await self.__receive_data()
|
|
84
83
|
if "Connection is busy. Bye!" in prompt:
|
|
85
84
|
return None
|
|
@@ -87,7 +86,7 @@ class InterCon:
|
|
|
87
86
|
if hostname is None or prompt is None or str(prompt).replace('$', '').strip() == str(hostname).split('.')[0]:
|
|
88
87
|
# Run command on validated device
|
|
89
88
|
# TODO: Handle multiple cmd as input, separated by ; (????)
|
|
90
|
-
self.writer.write(cmd)
|
|
89
|
+
self.writer.write(str.encode(' '.join(cmd)))
|
|
91
90
|
await self.writer.drain()
|
|
92
91
|
data, _ = await self.__receive_data(prompt=prompt)
|
|
93
92
|
if data == '\0':
|
|
@@ -141,7 +140,7 @@ class InterCon:
|
|
|
141
140
|
return data, prompt
|
|
142
141
|
|
|
143
142
|
|
|
144
|
-
async def _send_cmd(host, cmd, com_obj):
|
|
143
|
+
async def _send_cmd(host:str, cmd:list, com_obj):
|
|
145
144
|
"""
|
|
146
145
|
Async send command wrapper for further async task integration and sync send_cmd usage (main)
|
|
147
146
|
:param host: hostname / IP address
|
|
@@ -154,12 +153,12 @@ async def _send_cmd(host, cmd, com_obj):
|
|
|
154
153
|
out = await com_obj.send_cmd(host, cmd) # Send CMD
|
|
155
154
|
if out is not None: # Retry mechanism
|
|
156
155
|
break
|
|
157
|
-
await
|
|
156
|
+
await com_obj.task.feed(sleep_ms=100) # Retry mechanism
|
|
158
157
|
com_obj.task.out = '' if out is None else out
|
|
159
158
|
return com_obj.task.out
|
|
160
159
|
|
|
161
160
|
|
|
162
|
-
def send_cmd(host, cmd):
|
|
161
|
+
def send_cmd(host:str, cmd:list|str) -> dict:
|
|
163
162
|
"""
|
|
164
163
|
Sync wrapper of async _send_cmd (InterCon.send_cmd consumer with retry)
|
|
165
164
|
:param host: hostname / IP address
|
|
@@ -167,11 +166,15 @@ def send_cmd(host, cmd):
|
|
|
167
166
|
"""
|
|
168
167
|
def _tagify():
|
|
169
168
|
nonlocal host, cmd
|
|
170
|
-
_mod = cmd
|
|
169
|
+
_mod = cmd[0]
|
|
171
170
|
if InterCon.validate_ipv4(host):
|
|
172
171
|
return f"{'.'.join(host.split('.')[-2:])}.{_mod}"
|
|
173
172
|
return f"{host.replace('.local', '')}.{_mod}"
|
|
174
173
|
|
|
174
|
+
# Handle legacy string input
|
|
175
|
+
if isinstance(cmd, str):
|
|
176
|
+
cmd = cmd.split()
|
|
177
|
+
|
|
175
178
|
com_obj = InterCon()
|
|
176
179
|
tag = f"con.{_tagify()}"
|
|
177
180
|
started = com_obj.task.create(callback=_send_cmd(host, cmd, com_obj), tag=tag)
|
|
@@ -182,7 +185,7 @@ def send_cmd(host, cmd):
|
|
|
182
185
|
return result
|
|
183
186
|
|
|
184
187
|
|
|
185
|
-
def host_cache():
|
|
188
|
+
def host_cache() -> dict:
|
|
186
189
|
"""
|
|
187
190
|
Dump InterCon connection cache
|
|
188
191
|
"""
|
micrOS/source/LM_espnow.py
CHANGED
|
@@ -6,13 +6,14 @@ def load():
|
|
|
6
6
|
"""
|
|
7
7
|
return Espnow.initialize()
|
|
8
8
|
|
|
9
|
-
def send(peer, msg='modules'):
|
|
9
|
+
def send(peer:bytes|str, msg:str='modules'):
|
|
10
10
|
"""
|
|
11
11
|
Send message to peer (by mac address)
|
|
12
12
|
:param peer: mac address of espnow device
|
|
13
13
|
:param msg: message string/load module call
|
|
14
14
|
"""
|
|
15
|
-
|
|
15
|
+
now = Espnow.initialize()
|
|
16
|
+
return now.send(peer, msg)
|
|
16
17
|
|
|
17
18
|
def start_server():
|
|
18
19
|
"""
|
|
@@ -20,21 +21,23 @@ def start_server():
|
|
|
20
21
|
- this can receive espnow messages
|
|
21
22
|
- it includes Load Module execution logic (beta)
|
|
22
23
|
"""
|
|
23
|
-
|
|
24
|
+
now = Espnow.initialize()
|
|
25
|
+
return now.start_server()
|
|
24
26
|
|
|
25
27
|
def stats():
|
|
26
28
|
"""
|
|
27
29
|
Get ESPNOW stats
|
|
28
30
|
"""
|
|
29
|
-
|
|
31
|
+
now = Espnow.initialize()
|
|
32
|
+
return now.stats()
|
|
30
33
|
|
|
31
|
-
def add_peer(peer):
|
|
34
|
+
def add_peer(peer:bytes, dev_name:str=None):
|
|
32
35
|
"""
|
|
33
36
|
Add ESPNOW peer to known hosts
|
|
34
37
|
- It is needed before first send(...)
|
|
35
38
|
"""
|
|
36
39
|
now = Espnow.initialize()
|
|
37
|
-
return
|
|
40
|
+
return now.add_peer(peer, dev_name)
|
|
38
41
|
|
|
39
42
|
def mac_address():
|
|
40
43
|
"""
|
|
@@ -46,4 +49,4 @@ def help():
|
|
|
46
49
|
"""
|
|
47
50
|
[beta] ESPNOW sender/receiver with LM execution
|
|
48
51
|
"""
|
|
49
|
-
return 'load', 'send <peer> "ping"', 'start_server', 'add_peer <peer>', 'stats', 'mac_address'
|
|
52
|
+
return 'load', 'send <peer> "ping"', 'start_server', 'add_peer <peer> dev_name=None', 'stats', 'mac_address'
|
micrOS/source/LM_intercon.py
CHANGED
|
@@ -4,6 +4,7 @@ from Common import syslog
|
|
|
4
4
|
|
|
5
5
|
def sendcmd(*args, **kwargs):
|
|
6
6
|
"""
|
|
7
|
+
[!!!] OBSOLETE - NEW SYNTAX: system top >>node01.local
|
|
7
8
|
Implements send command function towards micrOS devices
|
|
8
9
|
example: sendcmd "hello" host="IP/hostname.local") OR sendcmd host="IP/hostname.local" cmd="system rssi")
|
|
9
10
|
:param host[0]: host IP or Hostname
|
|
@@ -34,6 +35,7 @@ def sendcmd(*args, **kwargs):
|
|
|
34
35
|
|
|
35
36
|
def addr_cache():
|
|
36
37
|
"""
|
|
38
|
+
[!!!] OBSOLETE -> new command: system hosts
|
|
37
39
|
Dump intercon connection cache
|
|
38
40
|
:return dict: device-ip pairs
|
|
39
41
|
"""
|
|
@@ -46,6 +48,7 @@ def addr_cache():
|
|
|
46
48
|
|
|
47
49
|
def help(widgets=False):
|
|
48
50
|
"""
|
|
51
|
+
[!!!] OBSOLETE
|
|
49
52
|
[i] micrOS LM naming convention - built-in help message
|
|
50
53
|
:return tuple:
|
|
51
54
|
(widgets=False) list of functions implemented by this application
|
micrOS/source/LM_light_sensor.py
CHANGED
|
@@ -5,12 +5,8 @@ ADC.ATTN_2_5DB — the full range voltage: 1.5V
|
|
|
5
5
|
ADC.ATTN_6DB — the full range voltage: 2.0V
|
|
6
6
|
ADC.ATTN_11DB — the full range voltage: 3.3V
|
|
7
7
|
"""
|
|
8
|
-
from Common import SmartADC, micro_task
|
|
8
|
+
from Common import SmartADC, micro_task, exec_cmd
|
|
9
9
|
from Types import resolve
|
|
10
|
-
try:
|
|
11
|
-
import LM_intercon as InterCon
|
|
12
|
-
except:
|
|
13
|
-
InterCon = None
|
|
14
10
|
from microIO import bind_pin, pinmap_search
|
|
15
11
|
|
|
16
12
|
|
|
@@ -76,19 +72,15 @@ async def _task(on, off, threshold, tolerance=2, check_ms=5000):
|
|
|
76
72
|
# TURN ON
|
|
77
73
|
if percent <= threshold:
|
|
78
74
|
if on != last_ev:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
InterCon.send_cmd(host, cmd)
|
|
83
|
-
my_task.out = f"{percent}% <= threshold: {threshold}% - ON"
|
|
75
|
+
host = on[0]
|
|
76
|
+
state, _ = exec_cmd(on[1:] + [f">>{host}"], jsonify=True, skip_check=True)
|
|
77
|
+
my_task.out = f"{percent}% <= threshold: {threshold}% - ON [{'OK' if state else 'NOK'}]"
|
|
84
78
|
last_ev = on
|
|
85
79
|
elif percent > threshold+tolerance: # +tolerance to avoid "on/off/on/off" on threshold limit
|
|
86
80
|
if off != last_ev:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
InterCon.send_cmd(host, cmd)
|
|
91
|
-
my_task.out = f"{percent}% > threshold: {threshold+tolerance}% - OFF"
|
|
81
|
+
host = off[0]
|
|
82
|
+
state, _ = exec_cmd(off[1:] + [f">>{host}"], jsonify=True, skip_check=True)
|
|
83
|
+
my_task.out = f"{percent}% > threshold: {threshold+tolerance}% - OFF [{'OK' if state else 'NOK'}]"
|
|
92
84
|
last_ev = off
|
|
93
85
|
await my_task.feed(sleep_ms=check_ms) # Sample every <check_ms> sec
|
|
94
86
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# async_message.py Test of asynchronous mqtt client with async Broker class
|
|
2
|
+
# (C) Copyright Peter Hinch 2024.
|
|
3
|
+
# Released under the MIT licence.
|
|
4
|
+
# Public brokers https://github.com/mqtt/mqtt.github.io/wiki/public_brokers
|
|
5
|
+
# mip command: ???
|
|
6
|
+
|
|
7
|
+
from Config import cfgget
|
|
8
|
+
from mqtt_as import MQTTClient, config
|
|
9
|
+
from Common import micro_task, console, syslog, exec_cmd
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Set up MQTT
|
|
13
|
+
class MQTT:
|
|
14
|
+
CLIENT:MQTTClient = None # MQTT Client (broker) instance
|
|
15
|
+
TOPIC = "micros" # Default topic
|
|
16
|
+
TOPIC_COMMAND_LUT = {} # Lookup table for command/topic pairs
|
|
17
|
+
# Example:
|
|
18
|
+
# {"topic1": ["mod func", "mod2 func"], "topic2": []}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _receiver(task):
|
|
22
|
+
"""
|
|
23
|
+
MQTT AS receiver loop
|
|
24
|
+
"""
|
|
25
|
+
async for topic, msg, retained in MQTT.CLIENT.queue:
|
|
26
|
+
topic, msg = topic.decode(), msg.decode()
|
|
27
|
+
console(f'Topic: "{topic}" Message: "{msg}" Retained: {retained}')
|
|
28
|
+
|
|
29
|
+
# Command execution... use MQTT.TOPIC_COMMAND_LUT
|
|
30
|
+
topic_commands:list = MQTT.TOPIC_COMMAND_LUT.get(topic, None)
|
|
31
|
+
output_struct:list = []
|
|
32
|
+
if topic_commands is None:
|
|
33
|
+
syslog(f"[WARN] mqtt Unknown topic: {topic}")
|
|
34
|
+
elif len(topic_commands) == 0:
|
|
35
|
+
syslog(f"[WARN] mqtt No commands for {topic}")
|
|
36
|
+
else:
|
|
37
|
+
task.out = f"Handle topic: {topic}"
|
|
38
|
+
for cmd in topic_commands:
|
|
39
|
+
single_command = cmd.split()
|
|
40
|
+
if len(single_command) > 0:
|
|
41
|
+
state, output = exec_cmd(single_command, jsonify=True, skip_check=True)
|
|
42
|
+
output_struct.append({"state": state, "result": output, "cmd": cmd})
|
|
43
|
+
if len(output_struct) > 0:
|
|
44
|
+
console(f'\tMQTT Publish: {output_struct}')
|
|
45
|
+
task.out = f"Publish {topic}"
|
|
46
|
+
MQTT.CLIENT.publish(topic, str(output_struct))
|
|
47
|
+
else:
|
|
48
|
+
task.out = f"Nothing to publish {topic}"
|
|
49
|
+
task.feed()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _subscribe():
|
|
53
|
+
"""
|
|
54
|
+
MQTT AS Topic subscribe towards server
|
|
55
|
+
"""
|
|
56
|
+
with micro_task(tag="mqtt.subscribe") as my_task:
|
|
57
|
+
my_task.out = "Started"
|
|
58
|
+
try:
|
|
59
|
+
for t in MQTT.TOPIC_COMMAND_LUT:
|
|
60
|
+
console(f"Subscribe topic: {t}")
|
|
61
|
+
await MQTT.CLIENT.subscribe(t, 1)
|
|
62
|
+
my_task.out = "Done"
|
|
63
|
+
except Exception as e:
|
|
64
|
+
my_task.out = f"Error: {e}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def _publish(message, topic):
|
|
68
|
+
"""
|
|
69
|
+
Send message to topic with mqtt
|
|
70
|
+
"""
|
|
71
|
+
tag = f"mqtt.publish.{topic}"
|
|
72
|
+
with micro_task(tag=tag) as my_task:
|
|
73
|
+
console(f"mqtt send: [{topic}] {message}")
|
|
74
|
+
await MQTT.CLIENT.publish(topic, message, qos=1)
|
|
75
|
+
my_task.out = "Sent"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def _up():
|
|
79
|
+
"""
|
|
80
|
+
UP Listener - resubscribe
|
|
81
|
+
"""
|
|
82
|
+
with micro_task(tag="mqtt.up") as my_task:
|
|
83
|
+
while True:
|
|
84
|
+
# Wait for UP Event - (re)subscribe
|
|
85
|
+
my_task.out = "Wait"
|
|
86
|
+
await MQTT.CLIENT.up.wait()
|
|
87
|
+
MQTT.CLIENT.up.clear()
|
|
88
|
+
micro_task(tag="mqtt.subscribe", task=_subscribe())
|
|
89
|
+
my_task.out = "Re-Subscription"
|
|
90
|
+
my_task.feed()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def _init_client(topic:str=None, commands:str=None, raw_dict:dict|None=None):
|
|
94
|
+
"""
|
|
95
|
+
Initialize main mqtt receiver and topics
|
|
96
|
+
:param topic: topic string, ex.: 'lights'
|
|
97
|
+
:param commands: semicolon separated commands. ex.: 'rgb toggle; cct toggle'
|
|
98
|
+
OR
|
|
99
|
+
:param raw_dict: python dict string for multi topic subscription, ex.: {"lights": ["rgb toggle", "cct toggle"], ...}
|
|
100
|
+
"""
|
|
101
|
+
with micro_task(tag="mqtt.client") as my_task:
|
|
102
|
+
try:
|
|
103
|
+
await MQTT.CLIENT.connect()
|
|
104
|
+
my_task.out = "Connection successful."
|
|
105
|
+
except OSError:
|
|
106
|
+
my_task.out = "Connection failed."
|
|
107
|
+
return
|
|
108
|
+
# Wait for mqtt client connected successfully
|
|
109
|
+
await MQTT.CLIENT.up.wait()
|
|
110
|
+
MQTT.CLIENT.up.clear()
|
|
111
|
+
# Initialize mqtt topics, ha
|
|
112
|
+
subscribe(topic, commands, raw_dict)
|
|
113
|
+
micro_task(tag="mqtt.up", task=_up())
|
|
114
|
+
# Async listener loop
|
|
115
|
+
await _receiver(my_task)
|
|
116
|
+
my_task.out = "Receiver closed"
|
|
117
|
+
# Close when listener exits
|
|
118
|
+
MQTT.CLIENT.close()
|
|
119
|
+
|
|
120
|
+
#########################################
|
|
121
|
+
# PUBLIC FUNCTIONS #
|
|
122
|
+
#########################################
|
|
123
|
+
|
|
124
|
+
def publish(message:str, topic:str=MQTT.TOPIC):
|
|
125
|
+
"""
|
|
126
|
+
Publish message
|
|
127
|
+
:param message: string to be sent
|
|
128
|
+
:param topic: topic for message
|
|
129
|
+
"""
|
|
130
|
+
state = micro_task(tag=f"mqtt.publish.{topic}", task=_publish(message, topic))
|
|
131
|
+
state = "starting" if state else "already running"
|
|
132
|
+
return f"Message send, {state}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def subscribe(topic:str=None, commands:str=None, raw_dict:dict|None=None):
|
|
136
|
+
"""
|
|
137
|
+
Subscribe for single topics and set callback function(s) aka command(s)
|
|
138
|
+
:param topic: topic string, ex.: 'lights'
|
|
139
|
+
:param commands: semicolon separated commands. ex.: 'rgb toggle; cct toggle'
|
|
140
|
+
OR
|
|
141
|
+
:param raw_dict: python dict string for multi topic subscription, ex.: {"lights": ["rgb toggle", "cct toggle"], ...}
|
|
142
|
+
|
|
143
|
+
return: all or selected topics command
|
|
144
|
+
"""
|
|
145
|
+
updated = False
|
|
146
|
+
topic = topic.strip()
|
|
147
|
+
# Register single topic
|
|
148
|
+
if topic and commands:
|
|
149
|
+
# raw commands structure: 'rgb toggle; cct toggle'
|
|
150
|
+
commands = [ c.strip() for c in commands.split(";") ]
|
|
151
|
+
# commands: Topic LUT structure: {'topic': ['mod func'], ..., 'lights': ['rgb toggle', 'cct toggle']}
|
|
152
|
+
updated = True if MQTT.TOPIC_COMMAND_LUT.get(topic, None) is None else False
|
|
153
|
+
MQTT.TOPIC_COMMAND_LUT[topic] = commands
|
|
154
|
+
# Register multiple topics at once
|
|
155
|
+
elif isinstance(raw_dict, dict):
|
|
156
|
+
updated = True
|
|
157
|
+
MQTT.TOPIC_COMMAND_LUT.update(raw_dict)
|
|
158
|
+
# Start subscribe task
|
|
159
|
+
if updated:
|
|
160
|
+
state = micro_task(tag="mqtt.subscribe", task=_subscribe())
|
|
161
|
+
state = "starting" if state else "already running"
|
|
162
|
+
return f"Subscribe, {state}"
|
|
163
|
+
|
|
164
|
+
# Return handling
|
|
165
|
+
if topic is not None:
|
|
166
|
+
# Return selected topic commands
|
|
167
|
+
return MQTT.TOPIC_COMMAND_LUT.get(topic, None)
|
|
168
|
+
# Return registered topics
|
|
169
|
+
return MQTT.TOPIC_COMMAND_LUT
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _configure(server_ip:str, username:str, password:str):
|
|
173
|
+
# Define configuration
|
|
174
|
+
config["keepalive"] = 120
|
|
175
|
+
config["queue_len"] = 1 # Use event interface with default queue
|
|
176
|
+
# Define configuration
|
|
177
|
+
config['client_id'] = cfgget("devfid")
|
|
178
|
+
config['ssid'] = cfgget("staessid")
|
|
179
|
+
config['wifi_pw'] = cfgget("stapwd")
|
|
180
|
+
config['port'] = 1883 # expose????
|
|
181
|
+
config['server'] = server_ip # '172.20.10.2'
|
|
182
|
+
config['user'] = username # test
|
|
183
|
+
config['password'] = password # '12345'
|
|
184
|
+
return config
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def load(server_ip:str, username:str, password:str, topic:str=None, commands:str=None, raw_dict:dict|None=None):
|
|
188
|
+
"""
|
|
189
|
+
Load MQTT_AS receiver...
|
|
190
|
+
:param server_ip: server IP address
|
|
191
|
+
:param username: server user
|
|
192
|
+
:param password: server user password
|
|
193
|
+
|
|
194
|
+
:param topic: topic string, ex.: 'lights'
|
|
195
|
+
:param commands: semicolon separated commands. ex.: 'rgb toggle; cct toggle'
|
|
196
|
+
OR
|
|
197
|
+
:param raw_dict: python dict string for multi topic subscription, ex.: {"lights": ["rgb toggle", "cct toggle"], ...}
|
|
198
|
+
"""
|
|
199
|
+
MQTTClient.DEBUG = True
|
|
200
|
+
MQTT.CLIENT = MQTTClient(_configure(server_ip, username, password))
|
|
201
|
+
|
|
202
|
+
state = micro_task(tag="mqtt.client", task=_init_client(topic, commands, raw_dict))
|
|
203
|
+
return "Starting" if state else "Already running"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def help():
|
|
207
|
+
return ("load <server_ip> <username> <password> topic='micros', commands='rgb toggle; cct toggle'",
|
|
208
|
+
"subscribe topic='micros', commands='rgb toggle; cct toggle'",
|
|
209
|
+
"subscribe #without params dumps the topic-command data structure",
|
|
210
|
+
"publish message='hello' topic='micros'",
|
|
211
|
+
"HINT: task show mqtt.*")
|
micrOS/source/LM_oled_ui.py
CHANGED
|
@@ -7,13 +7,9 @@ from Common import syslog, exec_cmd, manage_task
|
|
|
7
7
|
from machine import Pin
|
|
8
8
|
from Types import resolve
|
|
9
9
|
try:
|
|
10
|
-
from LM_system import memory_usage
|
|
10
|
+
from LM_system import memory_usage, hosts
|
|
11
11
|
except:
|
|
12
12
|
memory_usage = None # Optional function handling
|
|
13
|
-
try:
|
|
14
|
-
import LM_intercon as InterCon
|
|
15
|
-
except:
|
|
16
|
-
InterCon = None # Optional function handling
|
|
17
13
|
try:
|
|
18
14
|
from LM_gameOfLife import next_gen as gol_nextgen, reset as gol_reset
|
|
19
15
|
except:
|
|
@@ -228,6 +224,7 @@ class PageUI:
|
|
|
228
224
|
self.page_callback_list[self.active_page]() # <== Execute page functions
|
|
229
225
|
except Exception as e:
|
|
230
226
|
PageUI.PAGE_UI_OBJ.show_msg = f"Err: {e}" # Show page error in msgbox
|
|
227
|
+
syslog(f"oled_ui render error: {e}")
|
|
231
228
|
PageUI.DISPLAY.show()
|
|
232
229
|
self.__power_save()
|
|
233
230
|
else:
|
|
@@ -322,7 +319,7 @@ class PageUI:
|
|
|
322
319
|
#####################################
|
|
323
320
|
# PAGE GENERATORS #
|
|
324
321
|
#####################################
|
|
325
|
-
def intercon_page(self, host, cmd, run=False):
|
|
322
|
+
def intercon_page(self, host:str, cmd:list, run=False):
|
|
326
323
|
"""Generic interconnect page core - create multiple page with it"""
|
|
327
324
|
posx, posy = 5, 12
|
|
328
325
|
|
|
@@ -331,10 +328,13 @@ class PageUI:
|
|
|
331
328
|
self.open_intercons.append(host)
|
|
332
329
|
try:
|
|
333
330
|
# Send CMD to other device & show result
|
|
334
|
-
data_meta =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
331
|
+
state, data_meta = exec_cmd(cmd + [f">>{host}"], jsonify=True, skip_check=True)
|
|
332
|
+
if state:
|
|
333
|
+
self.cmd_task_tag = data_meta['tag']
|
|
334
|
+
if "Task is Busy" in data_meta['verdict'] and not run:
|
|
335
|
+
self.cmd_out = data_meta['verdict'] # Otherwise the task start output not relevant on UI
|
|
336
|
+
else:
|
|
337
|
+
self.cmd_out = f"Error: {data_meta}"
|
|
338
338
|
except Exception as e:
|
|
339
339
|
self.cmd_out = str(e)
|
|
340
340
|
self.open_intercons.remove(host)
|
|
@@ -344,7 +344,7 @@ class PageUI:
|
|
|
344
344
|
return
|
|
345
345
|
# Draw host + cmd details
|
|
346
346
|
PageUI.DISPLAY.text(host, 0, posy)
|
|
347
|
-
PageUI.DISPLAY.text(cmd, posx, posy+10)
|
|
347
|
+
PageUI.DISPLAY.text(' '.join(cmd), posx, posy+10)
|
|
348
348
|
# Update display output with retrieved task result (by TaskID)
|
|
349
349
|
if self.cmd_task_tag is not None:
|
|
350
350
|
task_buffer = manage_task(self.cmd_task_tag, 'show').replace(' ', '')
|
|
@@ -373,7 +373,7 @@ class PageUI:
|
|
|
373
373
|
try:
|
|
374
374
|
cmd_list = cmd.strip().split()
|
|
375
375
|
# Send CMD to other device & show result
|
|
376
|
-
state, out = exec_cmd(cmd_list)
|
|
376
|
+
state, out = exec_cmd(cmd_list, skip_check=True)
|
|
377
377
|
try:
|
|
378
378
|
self.cmd_out = ''.join(out.strip().split()).replace(' ', '')
|
|
379
379
|
except Exception:
|
|
@@ -409,13 +409,12 @@ def _sys_page():
|
|
|
409
409
|
|
|
410
410
|
|
|
411
411
|
def _intercon_cache(line_limit=3):
|
|
412
|
-
if InterCon is None:
|
|
413
|
-
return False
|
|
414
412
|
line_start = 15
|
|
415
413
|
line_cnt = 1
|
|
416
414
|
PageUI.DISPLAY.text("InterCon cache", 0, line_start)
|
|
417
|
-
|
|
418
|
-
|
|
415
|
+
cache = hosts()["intercon"]
|
|
416
|
+
if sum([1 for _ in cache]) > 0:
|
|
417
|
+
for key, val in cache.items():
|
|
419
418
|
key = key.split('.')[0]
|
|
420
419
|
val = '.'.join(val.split('.')[-2:])
|
|
421
420
|
PageUI.DISPLAY.text(f" {val} {key}", 0, line_start+(line_cnt*10))
|
|
@@ -502,7 +501,7 @@ def msgbox(msg='micrOS msg'):
|
|
|
502
501
|
return 'Show msg'
|
|
503
502
|
|
|
504
503
|
|
|
505
|
-
def intercon_genpage(cmd=None, run=False):
|
|
504
|
+
def intercon_genpage(cmd:str=None, run=False):
|
|
506
505
|
"""
|
|
507
506
|
Create intercon pages dynamically :)
|
|
508
507
|
- based on cmd value.
|
|
@@ -512,7 +511,7 @@ def intercon_genpage(cmd=None, run=False):
|
|
|
512
511
|
"""
|
|
513
512
|
raw = cmd.split()
|
|
514
513
|
host = raw[0]
|
|
515
|
-
cmd =
|
|
514
|
+
cmd = raw[1:]
|
|
516
515
|
try:
|
|
517
516
|
# Create page for intercon command
|
|
518
517
|
PageUI.PAGE_UI_OBJ.add_page(lambda: PageUI.PAGE_UI_OBJ.intercon_page(host, cmd, run=run))
|
|
@@ -522,7 +521,7 @@ def intercon_genpage(cmd=None, run=False):
|
|
|
522
521
|
return True
|
|
523
522
|
|
|
524
523
|
|
|
525
|
-
def cmd_genpage(cmd=None, run=False):
|
|
524
|
+
def cmd_genpage(cmd:str=None, run=False):
|
|
526
525
|
"""
|
|
527
526
|
Create load module execution pages dynamically :)
|
|
528
527
|
- based on cmd value: load_module function (args)
|
|
@@ -530,9 +529,6 @@ def cmd_genpage(cmd=None, run=False):
|
|
|
530
529
|
:param run: run button event at page init: True/False
|
|
531
530
|
:return: page creation verdict
|
|
532
531
|
"""
|
|
533
|
-
if not isinstance(cmd, str):
|
|
534
|
-
return False
|
|
535
|
-
|
|
536
532
|
try:
|
|
537
533
|
# Create page for intercon command
|
|
538
534
|
PageUI.PAGE_UI_OBJ.add_page(lambda: PageUI.PAGE_UI_OBJ.cmd_call_page(cmd, run=run))
|