micrOSDevToolKit 2.19.0__py3-none-any.whl → 2.21.0__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 (69) hide show
  1. micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +35 -35
  2. micrOS/source/Common.py +5 -13
  3. micrOS/source/Config.py +5 -2
  4. micrOS/source/Espnow.py +101 -45
  5. micrOS/source/Files.py +50 -23
  6. micrOS/source/InterConnect.py +10 -5
  7. micrOS/source/Pacman.py +141 -0
  8. micrOS/source/Shell.py +1 -1
  9. micrOS/source/Tasks.py +59 -54
  10. micrOS/source/modules/LM_buzzer.py +1 -4
  11. micrOS/source/modules/LM_cct.py +2 -4
  12. micrOS/source/modules/LM_dimmer.py +1 -2
  13. micrOS/source/modules/LM_distance.py +1 -3
  14. micrOS/source/modules/LM_i2s_mic.py +1 -2
  15. micrOS/source/modules/LM_keychain.py +1 -2
  16. micrOS/source/modules/LM_light_sensor.py +1 -4
  17. micrOS/source/modules/LM_mqtt_client.py +13 -10
  18. micrOS/source/modules/LM_neopixel.py +1 -2
  19. micrOS/source/modules/LM_oled_ui.py +39 -41
  20. micrOS/source/modules/LM_oledui.py +58 -89
  21. micrOS/source/modules/LM_pacman.py +36 -44
  22. micrOS/source/modules/LM_presence.py +1 -2
  23. micrOS/source/modules/LM_rest.py +1 -2
  24. micrOS/source/modules/LM_rgb.py +1 -2
  25. micrOS/source/modules/LM_roboarm.py +3 -4
  26. micrOS/source/modules/LM_robustness.py +1 -2
  27. micrOS/source/modules/LM_telegram.py +1 -2
  28. micrOS/source/urequests.py +1 -1
  29. {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/METADATA +153 -214
  30. {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/RECORD +67 -67
  31. toolkit/DevEnvOTA.py +2 -2
  32. toolkit/DevEnvUSB.py +1 -1
  33. toolkit/dashboard_apps/SystemTest.py +17 -14
  34. toolkit/lib/micrOSClient.py +37 -15
  35. toolkit/lib/micrOSClientHistory.py +35 -1
  36. toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
  37. toolkit/simulator_lib/uos.py +1 -0
  38. toolkit/workspace/precompiled/Common.mpy +0 -0
  39. toolkit/workspace/precompiled/Config.mpy +0 -0
  40. toolkit/workspace/precompiled/Espnow.mpy +0 -0
  41. toolkit/workspace/precompiled/Files.mpy +0 -0
  42. toolkit/workspace/precompiled/InterConnect.mpy +0 -0
  43. toolkit/workspace/precompiled/Pacman.mpy +0 -0
  44. toolkit/workspace/precompiled/Shell.mpy +0 -0
  45. toolkit/workspace/precompiled/Tasks.mpy +0 -0
  46. toolkit/workspace/precompiled/modules/LM_buzzer.mpy +0 -0
  47. toolkit/workspace/precompiled/modules/LM_cct.mpy +0 -0
  48. toolkit/workspace/precompiled/modules/LM_dimmer.mpy +0 -0
  49. toolkit/workspace/precompiled/modules/LM_distance.mpy +0 -0
  50. toolkit/workspace/precompiled/modules/LM_i2s_mic.mpy +0 -0
  51. toolkit/workspace/precompiled/modules/LM_keychain.mpy +0 -0
  52. toolkit/workspace/precompiled/modules/LM_light_sensor.mpy +0 -0
  53. toolkit/workspace/precompiled/modules/LM_mqtt_client.mpy +0 -0
  54. toolkit/workspace/precompiled/modules/LM_neopixel.mpy +0 -0
  55. toolkit/workspace/precompiled/modules/LM_oled_ui.mpy +0 -0
  56. toolkit/workspace/precompiled/modules/LM_oledui.mpy +0 -0
  57. toolkit/workspace/precompiled/modules/LM_pacman.mpy +0 -0
  58. toolkit/workspace/precompiled/modules/LM_presence.mpy +0 -0
  59. toolkit/workspace/precompiled/modules/LM_rest.mpy +0 -0
  60. toolkit/workspace/precompiled/modules/LM_rgb.mpy +0 -0
  61. toolkit/workspace/precompiled/modules/LM_roboarm.mpy +0 -0
  62. toolkit/workspace/precompiled/modules/LM_robustness.py +1 -2
  63. toolkit/workspace/precompiled/modules/LM_telegram.mpy +0 -0
  64. micrOS/source/modules/LM_pet_feeder.py +0 -78
  65. toolkit/workspace/precompiled/modules/LM_pet_feeder.py +0 -78
  66. {microsdevtoolkit-2.19.0.data → microsdevtoolkit-2.21.0.data}/scripts/devToolKit.py +0 -0
  67. {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/WHEEL +0 -0
  68. {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/licenses/LICENSE +0 -0
  69. {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/top_level.txt +0 -0
micrOS/source/Files.py CHANGED
@@ -3,41 +3,49 @@ Module is responsible high level micropython file system opeartions
3
3
  [IMPORTANT] This module must never use any micrOS specific functions or classes.
4
4
  """
5
5
 
6
- from uos import ilistdir, remove, stat, getcwd, mkdir
6
+ from uos import ilistdir, remove, stat, getcwd, mkdir, rmdir
7
7
  from sys import path as upath
8
8
 
9
9
  ################################ Helper functions #####################################
10
10
 
11
11
  def _filter(path:str='/', ext:tuple=None, prefix:tuple=None, hide_core:bool=True) -> bool:
12
12
  """
13
- Filter files
14
- :param path: file to check
13
+ Filter files in the micrOS filesystem.
14
+
15
+ :param path: file path to check
15
16
  :param ext: tuple of extensions to filter by, default: None (all)
16
- :param hide_core: hide core files (mpy, py), default: True
17
+ :param prefix: tuple of prefixes to match (e.g. ('LM', 'IO')), default: None
18
+ :param hide_core: if True, hides core .py/.mpy files in the root (current) directory
19
+ :return: bool, whether the file passes the filter
17
20
  """
21
+ parent = "/".join(path.split("/")[:-1]) or "/"
18
22
  fname = path.split("/")[-1]
19
23
  _ext = fname.split(".")[-1]
20
- if hide_core and _ext in ("mpy", "py") and not (fname.startswith("LM_") or fname.startswith("IO_")):
24
+
25
+ # --- Hide core logic ---
26
+ # Core = any .py/.mpy in the current (root) working directory
27
+ if hide_core and _ext in ("mpy", "py") and parent in ('/', ""):
21
28
  return False
29
+
30
+ # --- General matching rules ---
22
31
  if ext is None and prefix is None:
23
32
  return True
24
33
  if isinstance(prefix, tuple) and fname.split("_")[0] in prefix:
25
34
  return True
26
- if isinstance(ext, tuple) and fname.split(".")[-1] in ext:
35
+ if isinstance(ext, tuple) and _ext in ext:
27
36
  return True
28
37
  return False
29
38
 
30
39
  def is_protected(path:str='/') -> bool:
31
40
  """
32
- Check is file protected
33
- - deny deletion
41
+ Check is file/dir protected
42
+ - every file and folder is protected in root dir: /
43
+ - with protected file list
34
44
  """
35
- protected_entities = ("", "node_config.json", "modules", "config", "logs", "web", "data",
36
- "LM_pacman.mpy", "LM_system.mpy")
37
- entity = path.split("/")[-1].replace("/", "")
38
- if entity in protected_entities:
39
- return True
40
- return False
45
+ protected_files = ("node_config.json", "LM_system.mpy", "LM_pacman.mpy")
46
+ parent = "/".join(path.split("/")[:-1]) or "/"
47
+ fname = path.split("/")[-1]
48
+ return parent in ("/", "") or fname in protected_files
41
49
 
42
50
  def _type_mask_to_str(item_type:int=None) -> str:
43
51
  # Map the raw bit-mask to a single character
@@ -83,7 +91,7 @@ def ilist_fs(path:str="/", type_filter:str='*', select:str='*', core:bool=False)
83
91
  if type_filter in ("*", item_type):
84
92
  # Mods only
85
93
  _select = None if select == "*" else (select,)
86
- if item_type == 'f' and not _filter(item, prefix=_select, hide_core=not core):
94
+ if item_type == 'f' and not _filter(path_join(path, item), prefix=_select, hide_core=not core):
87
95
  continue
88
96
  if select != '*' and item_type == 'd':
89
97
  continue
@@ -102,20 +110,38 @@ def list_fs(path:str="/", type_filter:str='*', select:str='*', core:bool=False)
102
110
  return list(ilist_fs(path, type_filter, select, core))
103
111
 
104
112
 
105
- def remove_fs(path, allow_dir=False):
113
+ def remove_file(path, force=False):
106
114
  """
107
115
  Linux like rm command - delete app resources and folders
108
- :param path: app resource path
109
- :param allow_dir: enable directory deletion, default: False
116
+ :param path: file to delete
117
+ :param force: pypass file protection check - sudo mode
110
118
  """
111
119
  # protect some resources
112
- if is_protected(path):
113
- return f'{path} is protected, skip deletion'
114
- _is_file = is_file(path)
115
- if _is_file or allow_dir:
120
+ if not force and is_protected(path):
121
+ return f'Protected resource, skip deletion: {path}'
122
+ if is_file(path):
116
123
  remove(path)
117
124
  return f"{path} deleted"
118
- return f"Cannot delete {'file' if _is_file else 'dir'}: {path}"
125
+ return f"Cannot delete dir type: {path}"
126
+
127
+
128
+ def remove_dir(path, force=False):
129
+ """
130
+ Recursively delete a folder and all its contents.
131
+ :param path: folder to delete
132
+ :param force: pypass dir protection check - sudo mode
133
+ """
134
+ # protect some resources
135
+ if not force and is_protected(path):
136
+ return f'Protected resource, skip deletion: {path}'
137
+ for entry in ilistdir(path):
138
+ content_path = path_join(path, entry[0])
139
+ if is_dir(content_path): # directory flag
140
+ remove_dir(content_path)
141
+ else:
142
+ remove(content_path)
143
+ rmdir(path)
144
+ return f"{path} deleted"
119
145
 
120
146
 
121
147
  def path_join(*parts):
@@ -133,6 +159,7 @@ class OSPath:
133
159
  WEB = path_join(_ROOT,'/web') # Web resources (.html, .css, .js, .json, etc.)
134
160
  MODULES = path_join(_ROOT, '/modules') # Application modules (.mpy, .py)
135
161
  CONFIG = path_join(_ROOT, '/config') # System configuration files (node_config.json, etc.)
162
+ LIB = path_join(_ROOT, '/lib') # Official and Custom package installation target path
136
163
 
137
164
 
138
165
  def init_micros_dirs():
@@ -13,6 +13,7 @@ Designed by
13
13
  from socket import getaddrinfo, SOCK_STREAM
14
14
  from re import compile as re_compile
15
15
  from json import loads
16
+ from binascii import hexlify
16
17
  from uasyncio import open_connection
17
18
 
18
19
  from Debug import syslog
@@ -170,7 +171,7 @@ class InterCon:
170
171
  """
171
172
  response = await self.send_cmd(host, ["task", "list", ">json"])
172
173
  if not response:
173
- return {None: f"[ERR] ESPNow auto handshake: task list, {response}"}
174
+ return {None: f"[ERR] ESPNow auto handshake: task list >>{host}: {response}"}
174
175
 
175
176
  active_tasks = loads(response).get("active")
176
177
  if "espnow.server" in active_tasks:
@@ -254,16 +255,20 @@ def send_cmd(host:str, cmd:list|str) -> dict:
254
255
  _mod = cmd[0]
255
256
  if InterCon.validate_ipv4(host):
256
257
  return f"{'.'.join(host.split('.')[-2:])}.{_mod}"
257
- return f"{host.replace('.local', '')}.{_mod}"
258
+ target = ".".join(host.split(".")[0:-1]) if "." in host else host
259
+ return f"{target}.{_mod}"
258
260
 
259
261
  com_obj = InterCon()
260
262
  task_id = f"con.{_tagify()}" # CHECK TASK ID CONFLICT
261
- state = com_obj.task.create(callback=_send_cmd(host, cmd, com_obj), tag=task_id)
262
- return {task_id: "Starting"} if state else {task_id: "Already running"}
263
+ return com_obj.task.create(callback=_send_cmd(host, cmd, com_obj), tag=task_id)
263
264
 
264
265
 
265
266
  def host_cache() -> dict:
266
267
  """
267
268
  Dump InterCon connection cache
268
269
  """
269
- return InterCon.CONN_MAP
270
+ if ESPNowSS is None:
271
+ return InterCon.CONN_MAP
272
+ all_devs = dict({name:hexlify(mac, ':').decode() for mac, name in ESPNowSS().devices.items()})
273
+ all_devs.update(InterCon.CONN_MAP)
274
+ return all_devs
@@ -0,0 +1,141 @@
1
+ from mip import install
2
+ from Files import OSPath, path_join, is_file
3
+ from Debug import syslog
4
+
5
+
6
+ # ---------------------------------------------------------------------
7
+ # Utility helpers
8
+ # ---------------------------------------------------------------------
9
+
10
+ def _normalize_source(ref):
11
+ """
12
+ Normalize GitHub URLs or shorthand for mip compatibility.
13
+ Converts:
14
+ - https://github.com/user/repo/blob/branch/path/file.py → https://raw.githubusercontent.com/user/repo/branch/path/file.py
15
+ - https://github.com/user/repo/tree/branch/path → github:user/repo/path
16
+ Returns (normalized_ref, branch)
17
+ """
18
+ try:
19
+ ref = ref.strip().rstrip("/")
20
+ # Already in github: shorthand
21
+ if ref.startswith("github:"):
22
+ return ref, None
23
+
24
+ if ref.startswith("https://"):
25
+ ref = ref.replace("https://", "")
26
+ if ref.startswith("github.com"):
27
+ # Folder (tree) case → github:user/repo/path
28
+ if "/tree/" in ref:
29
+ print("[mip-normalize] detected GitHub tree folder link")
30
+ parts = ref.split("/")
31
+ user, repo = parts[1], parts[2]
32
+ branch = parts[4]
33
+ path = "/".join(parts[5:])
34
+ github_ref = f"github:{user}/{repo}/{path}".rstrip("/")
35
+ return github_ref, branch
36
+
37
+ # File (blob) case → raw.githubusercontent.com
38
+ if "/blob/" in ref:
39
+ print("[mip-normalize] detected GitHub blob file link")
40
+ url_base = "https://raw.githubusercontent.com/"
41
+ ref = ref.replace("github.com/", url_base).replace("/blob", "")
42
+ return ref, None
43
+
44
+ # Direct GitHub file (no blob/tree) → github:user/repo/path
45
+ if ref.count("/") >= 2:
46
+ print("[mip-normalize] direct GitHub path (no blob/tree)")
47
+ parts = ref.split("/")
48
+ user, repo = parts[1], parts[2]
49
+ path = "/".join(parts[3:])
50
+ github_ref = f"github:{user}/{repo}/{path}".rstrip("/")
51
+ return github_ref, None
52
+
53
+ print("[mip-normalize] unchanged")
54
+ return ref, None
55
+
56
+ except Exception as e:
57
+ syslog(f"[ERR][pacman] normalize failed: {ref}: {e}")
58
+ print(f"[normalize][ERROR] {e}")
59
+ return str(ref), None
60
+
61
+
62
+ # ---------------------------------------------------------------------
63
+ # Core installer
64
+ # ---------------------------------------------------------------------
65
+
66
+ def _install_any(ref, target=None):
67
+ """Internal wrapper with consistent error handling and debug output."""
68
+ verdict = f"[mip] Installing: {ref}\n"
69
+ try:
70
+ ref, branch = _normalize_source(ref)
71
+ kwargs = {}
72
+ if branch:
73
+ kwargs["version"] = branch
74
+ kwargs["target"] = target or OSPath.LIB
75
+ verdict = f"[mip] Installing: {ref} {kwargs}\n"
76
+ # MIP Install
77
+ install(ref, **kwargs)
78
+ verdict += f" ✓ Installed successfully under {kwargs["target"]}"
79
+ except Exception as e:
80
+ err = f" ✗ Failed to install '{ref}': {e}"
81
+ syslog(f"[ERR][pacman] {err}")
82
+ verdict += err
83
+ return verdict
84
+
85
+
86
+ # ---------------------------------------------------------------------
87
+ # Public install functions
88
+ # ---------------------------------------------------------------------
89
+
90
+ def install_requirements(source="requirements.txt"):
91
+ """Install from a requirements.txt file under /config."""
92
+ verdict = f"[mip] Installing from requirements file: {source}\n"
93
+ try:
94
+ source_path = path_join(OSPath.CONFIG, source)
95
+ verdict = f"[mip] Installing from requirements file: {source_path}\n"
96
+ if is_file(source_path):
97
+ install(source_path)
98
+ verdict += " ✓ All listed packages processed"
99
+ else:
100
+ err = f" ✗ {source_path} not exists"
101
+ syslog(f"[ERR][pacman] {err}")
102
+ verdict += err
103
+ except Exception as e:
104
+ err = f" ✗ Failed to process {source}: {e}"
105
+ syslog(f"[ERR][pacman] {err}")
106
+ verdict += err
107
+ return verdict
108
+
109
+
110
+ # ---------------------------------------------------------------------
111
+ # Unified entry point
112
+ # ---------------------------------------------------------------------
113
+
114
+ def download(ref):
115
+ """
116
+ Unified mip-based downloader for micrOS.
117
+ Automatically detects:
118
+ - requirements.txt files (local or remote)
119
+ - Single-file load modules (LM_/IO_ names or URLs)
120
+ - GitHub or raw URLs (tree/blob/github:)
121
+ - Official MicroPython packages
122
+ """
123
+
124
+ if not ref:
125
+ return "[mip] Nothing to download (empty input)"
126
+
127
+ # 1. requirements.txt
128
+ if ref == "requirements.txt":
129
+ return install_requirements(ref)
130
+
131
+ if "github" in ref:
132
+ # 2. LM_/IO_ load modules → /modules
133
+ if ref.endswith("py") and ("LM_" in ref or "IO_" in ref):
134
+ return _install_any(ref, target=OSPath.MODULES)
135
+
136
+ # 3. GitHub or raw URLs → /lib
137
+ if ref.startswith("http") or ref.startswith("github"):
138
+ return _install_any(ref, target=OSPath.LIB)
139
+
140
+ # 4. Fallback: official micropython package → /lib
141
+ return _install_any(ref, target=OSPath.LIB)
micrOS/source/Shell.py CHANGED
@@ -25,7 +25,7 @@ from Debug import syslog
25
25
 
26
26
  class Shell:
27
27
  __slots__ = ['__devfid', '__auth_mode', '__hwuid', '__auth_ok', '__conf_mode']
28
- MICROS_VERSION = '2.19.0-0'
28
+ MICROS_VERSION = '2.21.0-0'
29
29
 
30
30
  def __init__(self):
31
31
  """
micrOS/source/Tasks.py CHANGED
@@ -10,6 +10,7 @@ Designed by Marcell Ban aka BxNxM
10
10
  #################################################################
11
11
  from sys import modules
12
12
  from json import dumps
13
+ from re import match
13
14
  import uasyncio as asyncio
14
15
  from micropython import schedule
15
16
  from utime import ticks_ms, ticks_diff
@@ -18,10 +19,10 @@ from Config import cfgget
18
19
  from Network import sta_high_avail
19
20
 
20
21
  try:
21
- from gc import collect
22
+ from gc import collect as gcollect
22
23
  except ImportError:
23
24
  console_write("[SIMULATOR MODE GC IMPORT]")
24
- from simgc import collect
25
+ from simgc import collect as gcollect
25
26
 
26
27
  #################################################################
27
28
  # Implement custom task class #
@@ -43,32 +44,20 @@ class TaskBase:
43
44
  self.done = asyncio.Event() # Store task done state
44
45
  self.out = "" # Store task output
45
46
 
46
- def _create(self, callback:callable) -> bool:
47
+ ###### BASE METHODS FOR CHILD CLASSES ####
48
+ def _create(self, callback:callable) -> dict:
47
49
  """
48
50
  Create async task and register it to TASKS dict by tag
49
51
  :param callback: coroutine function
50
52
  """
51
- if TaskBase.is_busy(self.tag):
52
- # Skip task if already running
53
- return False
54
53
  # Create async task from coroutine function
55
54
  self.task = asyncio.get_event_loop().create_task(callback)
56
55
  # Store Task object by key - for task control
57
56
  TaskBase.TASKS[self.tag] = self
58
- return True
59
-
60
- @staticmethod
61
- def is_busy(tag:str) -> bool:
62
- """
63
- Check task is busy by tag
64
- :param tag: for task selection
65
- """
66
- task = TaskBase.TASKS.get(tag, None)
67
- # return True: busy OR False: not busy (inactive) OR None: not exists
68
- return bool(task is not None and not task.done.is_set())
57
+ return {self.tag: "Starting"}
69
58
 
70
59
  @staticmethod
71
- def task_gc():
60
+ def _task_gc():
72
61
  """
73
62
  Automatic passive task deletion over QUEUE_SIZE
74
63
  """
@@ -77,18 +66,18 @@ class TaskBase:
77
66
  if len(passive) >= keep:
78
67
  for i in range(0, len(passive)-keep+1):
79
68
  del TaskBase.TASKS[passive[i]]
80
- collect() # GC collect
69
+ gcollect()
81
70
 
71
+ ###### PUBLIC TASK METHODS #####
82
72
  @staticmethod
83
- async def feed(sleep_ms=1):
73
+ def is_busy(tag:str) -> bool:
84
74
  """
85
- Feed event loop
86
- :param sleep_ms: in millisecond
75
+ Check task is busy by tag
76
+ :param tag: for task selection
87
77
  """
88
- # TODO: feed WDT - preemptive cooperative multitasking aka reboot if no feed until X time period
89
- if sleep_ms <= 0:
90
- return await asyncio.sleep(0.000_000_1) # 0 means: 100ns (Absolute minimum)
91
- return await asyncio.sleep_ms(sleep_ms)
78
+ task = TaskBase.TASKS.get(tag, None)
79
+ # return True: busy OR False: not busy (inactive) OR None: not exists
80
+ return bool(task is not None and not task.done.is_set())
92
81
 
93
82
  def cancel(self) -> bool:
94
83
  """
@@ -109,15 +98,16 @@ class TaskBase:
109
98
  return False
110
99
  return True
111
100
 
112
- def __task_del(self, keep_cache=False):
101
+ @staticmethod
102
+ async def feed(sleep_ms=1):
113
103
  """
114
- Delete task from TASKS
104
+ Feed event loop
105
+ :param sleep_ms: in millisecond
115
106
  """
116
- self.done.set()
117
- if self.tag in TaskBase.TASKS:
118
- if not keep_cache: # True - In case of destructor
119
- del TaskBase.TASKS[self.tag]
120
- collect() # GC collect
107
+ # TODO?: feed WDT - auto restart when system is frozen
108
+ if sleep_ms <= 0:
109
+ return await asyncio.sleep(0.000_000_1) # 0 means: 100ns (Absolute minimum)
110
+ return await asyncio.sleep_ms(sleep_ms)
121
111
 
122
112
  async def await_result(self, timeout:int=5):
123
113
  """
@@ -130,6 +120,17 @@ class TaskBase:
130
120
  return "Timeout has beed exceeded"
131
121
  return self.out
132
122
 
123
+ ###### PRIVATE LCM METHODS #####
124
+ def __task_del(self, keep_cache=False):
125
+ """
126
+ Delete task from TASKS
127
+ """
128
+ self.done.set()
129
+ if self.tag in TaskBase.TASKS:
130
+ if not keep_cache: # True - In case of destructor
131
+ del TaskBase.TASKS[self.tag]
132
+ gcollect()
133
+
133
134
  def __del__(self):
134
135
  try:
135
136
  self.__task_del(keep_cache=True)
@@ -143,7 +144,7 @@ class NativeTask(TaskBase):
143
144
  - could be built in function or custom code from load modules
144
145
  """
145
146
 
146
- def create(self, callback:callable=None, tag:str=None) -> bool:
147
+ def create(self, callback:callable=None, tag:str=None) -> dict:
147
148
  """
148
149
  Create async task with coroutine callback (no queue limit check!)
149
150
  + async socket server task
@@ -152,6 +153,9 @@ class NativeTask(TaskBase):
152
153
  """
153
154
  # Create task tag
154
155
  self.tag = f"aio.{ticks_ms()}" if tag is None else tag
156
+ if self.is_busy(self.tag):
157
+ # Skip task if already running
158
+ return {self.tag: "Already running"}
155
159
  # Create task with coroutine callback
156
160
  return super()._create(callback)
157
161
 
@@ -170,7 +174,7 @@ class NativeTask(TaskBase):
170
174
  Helper function for Task creation in Load Modules
171
175
  [HINT] Use python with feature to utilize this feature
172
176
  """
173
- self.task_gc() # Task pool cleanup
177
+ self._task_gc() # Task pool cleanup
174
178
  self.done.set()
175
179
 
176
180
 
@@ -186,7 +190,7 @@ class MagicTask(TaskBase):
186
190
  self.__inloop = False # [LM] Task while loop for LM callback
187
191
  self.__sleep = 20 # [LM] Task while loop - async wait (proc feed) [ms]
188
192
 
189
- def create(self, callback:list=None, loop:bool=None, sleep:int=None) -> bool:
193
+ def create(self, callback:list=None, loop:bool=None, sleep:int=None) -> dict:
190
194
  """
191
195
  Create async task with function callback (with queue limit check)
192
196
  - wrap (sync) function into async task (task_wrapper)
@@ -196,13 +200,14 @@ class MagicTask(TaskBase):
196
200
  """
197
201
  # Create task tag
198
202
  self.tag = '.'.join(callback[0:2])
199
-
203
+ if self.is_busy(self.tag):
204
+ # Skip task if already running
205
+ return {self.tag: "Already running"}
200
206
  # Set parameters for async wrapper
201
207
  self.__callback = callback
202
208
  self.__inloop = self.__inloop if loop is None else loop
203
209
  # Set sleep value for async loop - optional parameter with min sleep limit check (20ms)
204
210
  self.__sleep = self.__sleep if sleep is None else sleep if sleep > 19 else self.__sleep
205
-
206
211
  # Create task with coroutine callback
207
212
  return super()._create(self.__task_wrapper())
208
213
 
@@ -219,7 +224,7 @@ class MagicTask(TaskBase):
219
224
  state, self.out = _exec_lm_core(self.__callback)
220
225
  if not state or not self.__inloop:
221
226
  break
222
- self.task_gc() # Task pool cleanup
227
+ self._task_gc() # Task pool cleanup
223
228
  self.done.set()
224
229
 
225
230
  def cancel(self):
@@ -310,12 +315,16 @@ class Manager:
310
315
  my_task.done.set()
311
316
 
312
317
  @staticmethod
313
- def create_task(callback, tag:str=None, loop:bool=False, delay:int=None):
318
+ def create_task(callback, tag:str=None, loop:bool=False, delay:int=None) -> dict:
314
319
  """
315
- Primary interface
316
- Generic task creator method
317
- Create async Task with coroutine/list(lm call) callback
318
- :param callback: list|callable
320
+ Primary interface of micrOS Generic task creator method
321
+ :param tag: task unique identifier
322
+ NativeTask:
323
+ :param callback: callable, coroutine to start a task
324
+ MagicTask with queue limiter:
325
+ :param callback: list of staring (command)
326
+ :param loop: MagicTask looping parameter
327
+ :param delay: MagicTask delay parameter
319
328
  """
320
329
  if isinstance(callback, list):
321
330
  # Check queue if task is Load Module
@@ -450,8 +459,10 @@ def exec_builtins(func):
450
459
  # ... >json - command output format option
451
460
  # ... >>node01.local - intercon: command execution on remote device by hostname/IP address
452
461
  arg_list, json_flag = (arg_list[:-1], True) if arg_list[-1] == '>json' else (arg_list, False)
453
- arg_list, intercon_target = (arg_list[:-1], arg_list[-1].replace(">>", "")) if arg_list[-1].startswith('>>') else (arg_list, None)
454
462
  json_flag = jsonify if isinstance(jsonify, bool) else json_flag
463
+ arg_list, intercon_target = ((arg_list[:-1], arg_list[-1].replace(">>", ""))
464
+ if match(r'^>>[A-Za-z0-9._-]+$', arg_list[-1])
465
+ else (arg_list, None))
455
466
 
456
467
  # INTERCONNECT
457
468
  if intercon_target:
@@ -513,15 +524,9 @@ def lm_exec(arg_list:list, jsonify:bool=None):
513
524
  delay = int(delay) if delay.isdigit() else None
514
525
  # Create and start async lm task
515
526
  try:
516
- state = Manager.create_task(arg_list, loop=loop, delay=delay)
527
+ return True, Manager.create_task(arg_list, loop=loop, delay=delay)
517
528
  except Exception as e:
518
- # Valid & handled task command
519
- return True, str(e)
520
- tag = '.'.join(arg_list[0:2])
521
- # Valid & handled task command
522
- if state:
523
- return True, f"Start {tag}"
524
- return True, f"{tag} is Busy"
529
+ return False, {".".join(arg_list[0:2]): str(e)}
525
530
 
526
531
  # [2] Sync "realtime" task execution
527
532
  state, out = _exec_lm_core(arg_list, jsonify)
@@ -590,7 +595,7 @@ def _exec_lm_core(cmd_list, jsonify):
590
595
  # UNLOAD MODULE IF MEMORY ERROR HAPPENED + gc.collect
591
596
  if lm_mod in modules:
592
597
  del modules[lm_mod]
593
- collect()
598
+ gcollect()
594
599
  # LM EXECUTION ERROR
595
600
  return False, f"Core error: {lm_mod}->{lm_func}: {e}"
596
601
  return False, "Shell: for hints type help.\nShell: for LM exec: [1](LM)module [2]function [3...]optional params"
@@ -270,10 +270,7 @@ def play(rtttlstr='Indiana'):
270
270
  global CHECK_NOTIFY
271
271
  if CHECK_NOTIFY and not notify():
272
272
  return "NoBipp - notify off"
273
- state = micro_task(tag=__TASK_TAG, task=_play(rtttlstr))
274
- if state:
275
- return 'Play song'
276
- return 'Song already playing'
273
+ return micro_task(tag=__TASK_TAG, task=_play(rtttlstr))
277
274
 
278
275
 
279
276
  def list_tones():
@@ -238,8 +238,7 @@ def transition(cw=None, ww=None, sec=1.0, wake=False):
238
238
  # Create transition generator and calculate step_ms
239
239
  cct_gen, step_ms = transition_gen(cw_from, cw_to, ww_from, ww_to, interval_sec=sec)
240
240
  # [!] ASYNC TASK CREATION [1*] with async task callback + taskID (TAG) handling
241
- state = micro_task(tag=Data.CCT_TASK_TAG, task=_task(ms_period=step_ms, iterable=cct_gen))
242
- return "Starting transition" if state else "Transition already running"
241
+ return micro_task(tag=Data.CCT_TASK_TAG, task=_task(ms_period=step_ms, iterable=cct_gen))
243
242
 
244
243
 
245
244
  def hue_transition(percent, sec=1.0, wake=False):
@@ -288,8 +287,7 @@ def hue_transition(percent, sec=1.0, wake=False):
288
287
  #print("Actual percent: {}, target percent: {}".format(actual_percent, warm_percent))
289
288
  hue_gen, step_ms = transition_gen(hue_curr_percent, percent*10, interval_sec=sec)
290
289
  # [!] ASYNC TASK CREATION [1*] with async task callback + taskID (TAG) handling
291
- state = micro_task(tag=Data.HUE_TASK_TAG, task=_task(ms_period=step_ms, iterable=hue_gen))
292
- return "Starting transition" if state else "Transition already running"
290
+ return micro_task(tag=Data.HUE_TASK_TAG, task=_task(ms_period=step_ms, iterable=hue_gen))
293
291
  else:
294
292
  return "Invalid range, percent=<0-100>"
295
293
 
@@ -184,8 +184,7 @@ def transition(value, sec=1.0, wake=False):
184
184
  # Create transition generator and calculate step_ms
185
185
  fade_gen, fade_step_ms = transition_gen(from_dim, value, interval_sec=sec)
186
186
  # [!] ASYNC TASK CREATION [1*] with async task callback + taskID (TAG) handling
187
- state = micro_task(tag=Data.DIMM_TASK_TAG, task=_task(ms_period=fade_step_ms, iterable=fade_gen))
188
- return "Starting transition" if state else "Transition already running"
187
+ return micro_task(tag=Data.DIMM_TASK_TAG, task=_task(ms_period=fade_step_ms, iterable=fade_gen))
189
188
 
190
189
 
191
190
  def subscribe_presence():
@@ -70,9 +70,7 @@ def start_dimmer_indicator(idle_distance=180):
70
70
  """Distance visualization on LED brightness (LM_dimmer)"""
71
71
  from LM_dimmer import set_value
72
72
  # [!] ASYNC TASK CREATION [1*] with async task callback + taskID (TAG) handling
73
- state = micro_task(tag="distance.visual", task=__task(period_ms=200, dimmer=set_value, idle_cm=idle_distance))
74
- return "Starting" if state else "Already running"
75
-
73
+ return micro_task(tag="distance.visual", task=__task(period_ms=200, dimmer=set_value, idle_cm=idle_distance))
76
74
 
77
75
 
78
76
  #########################
@@ -94,8 +94,7 @@ def background_capture():
94
94
  if not Data.MIC_ENABLED:
95
95
  return "Microphone is disabled"
96
96
 
97
- state = micro_task(tag=Data.TASK_TAG, task=__task(ms_period=1))
98
- return "Starting" if state else "Already running"
97
+ return micro_task(tag=Data.TASK_TAG, task=__task(ms_period=1))
99
98
 
100
99
 
101
100
  def get_from_buffer(capture_duration=Data.CAPTURE_DURATION,
@@ -216,8 +216,7 @@ def display(period=1000, tts=30):
216
216
  # [!] ASYNC TASK CREATION [1*] with async task callback + taskID (TAG) handling
217
217
  period_ms = 500 if period < 500 else period
218
218
  tts_ms = 5000 if tts < 5 else tts*1000
219
- state = micro_task(tag="keychain.display", task=_ui_task(period_ms, tts_ms))
220
- return "Starting" if state else "Already running"
219
+ return micro_task(tag="keychain.display", task=_ui_task(period_ms, tts_ms))
221
220
 
222
221
 
223
222
  def temperature():
@@ -96,11 +96,8 @@ def subscribe_intercon(on, off, threshold=4, tolerance=2, sample_sec=60):
96
96
  """
97
97
  # Start play - servo XY in async task
98
98
  # [!] ASYNC TASK CREATION [1*] with async task callback + taskID (TAG) handling
99
- state = micro_task(tag="light_sensor.intercon", task=_task(on, off, threshold, tolerance=tolerance,
99
+ return micro_task(tag="light_sensor.intercon", task=_task(on, off, threshold, tolerance=tolerance,
100
100
  check_ms=sample_sec*1000))
101
- if state:
102
- return 'Light sensor remote trigger starts'
103
- return 'Light sensor remote trigger - already running'
104
101
 
105
102
 
106
103
  #######################