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.

Files changed (54) hide show
  1. micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +37 -29
  2. micrOS/source/Common.py +5 -5
  3. micrOS/source/Espnow.py +245 -123
  4. micrOS/source/Files.py +101 -0
  5. micrOS/source/InterConnect.py +14 -11
  6. micrOS/source/LM_espnow.py +10 -7
  7. micrOS/source/LM_intercon.py +3 -0
  8. micrOS/source/LM_light_sensor.py +7 -15
  9. micrOS/source/LM_mqtt_pro.py +211 -0
  10. micrOS/source/LM_oled_ui.py +18 -22
  11. micrOS/source/LM_oledui.py +13 -16
  12. micrOS/source/LM_pacman.py +37 -57
  13. micrOS/source/LM_presence.py +8 -11
  14. micrOS/source/LM_system.py +8 -7
  15. micrOS/source/LM_telegram.py +21 -16
  16. micrOS/source/Logger.py +5 -11
  17. micrOS/source/Shell.py +37 -30
  18. micrOS/source/Tasks.py +32 -17
  19. micrOS/source/Web.py +3 -2
  20. micrOS/source/__pycache__/Common.cpython-312.pyc +0 -0
  21. micrOS/source/__pycache__/Logger.cpython-312.pyc +0 -0
  22. micrOS/source/microIO.py +3 -2
  23. micrOS/source/urequests.py +10 -1
  24. {micrOSDevToolKit-2.9.11.dist-info → microsdevtoolkit-2.10.5.dist-info}/METADATA +13 -15
  25. {micrOSDevToolKit-2.9.11.dist-info → microsdevtoolkit-2.10.5.dist-info}/RECORD +54 -50
  26. {micrOSDevToolKit-2.9.11.dist-info → microsdevtoolkit-2.10.5.dist-info}/WHEEL +1 -1
  27. toolkit/dashboard_apps/SystemTest.py +17 -6
  28. toolkit/lib/micrOSClient.py +25 -6
  29. toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
  30. toolkit/simulator_lib/uos.py +5 -5
  31. toolkit/socketClient.py +2 -3
  32. toolkit/workspace/precompiled/Common.mpy +0 -0
  33. toolkit/workspace/precompiled/Espnow.mpy +0 -0
  34. toolkit/workspace/precompiled/Files.mpy +0 -0
  35. toolkit/workspace/precompiled/InterConnect.mpy +0 -0
  36. toolkit/workspace/precompiled/LM_espnow.py +10 -7
  37. toolkit/workspace/precompiled/LM_intercon.mpy +0 -0
  38. toolkit/workspace/precompiled/LM_light_sensor.mpy +0 -0
  39. toolkit/workspace/precompiled/LM_mqtt_pro.py +211 -0
  40. toolkit/workspace/precompiled/LM_oled_ui.mpy +0 -0
  41. toolkit/workspace/precompiled/LM_oledui.mpy +0 -0
  42. toolkit/workspace/precompiled/LM_pacman.mpy +0 -0
  43. toolkit/workspace/precompiled/LM_presence.mpy +0 -0
  44. toolkit/workspace/precompiled/LM_system.mpy +0 -0
  45. toolkit/workspace/precompiled/LM_telegram.mpy +0 -0
  46. toolkit/workspace/precompiled/Logger.mpy +0 -0
  47. toolkit/workspace/precompiled/Shell.mpy +0 -0
  48. toolkit/workspace/precompiled/Tasks.mpy +0 -0
  49. toolkit/workspace/precompiled/Web.mpy +0 -0
  50. toolkit/workspace/precompiled/microIO.mpy +0 -0
  51. toolkit/workspace/precompiled/urequests.mpy +0 -0
  52. {micrOSDevToolKit-2.9.11.data → microsdevtoolkit-2.10.5.data}/scripts/devToolKit.py +0 -0
  53. {micrOSDevToolKit-2.9.11.dist-info → microsdevtoolkit-2.10.5.dist-info/licenses}/LICENSE +0 -0
  54. {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
@@ -1,6 +1,6 @@
1
1
  from socket import getaddrinfo, SOCK_STREAM
2
2
  from re import compile
3
- import uasyncio as asyncio
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 asyncio.open_connection(host, InterCon.PORT)
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 asyncio.sleep_ms(100) # Retry mechanism
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.split(' ')[0].strip()
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
  """
@@ -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
- return Espnow.espnow_send(peer, msg)
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
- return Espnow.espnow_server()
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
- return Espnow.stats()
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 Espnow.add_peer(now, peer)
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'
@@ -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
@@ -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
- if InterCon is not None:
80
- host = on[0]
81
- cmd = ' '.join(on[1:])
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
- if InterCon is not None:
88
- host = off[0]
89
- cmd = ' '.join(off[1:])
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.*")
@@ -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 = InterCon.send_cmd(host, cmd)
335
- self.cmd_task_tag = data_meta['tag']
336
- if "Task is Busy" in data_meta['verdict'] and not run:
337
- self.cmd_out = data_meta['verdict'] # Otherwise the task start output not relevant on UI
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
- if sum([1 for _ in InterCon.host_cache()]) > 0:
418
- for key, val in InterCon.host_cache().items():
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 = ' '.join(raw[1:])
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))