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.
- micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +35 -35
- micrOS/source/Common.py +5 -13
- micrOS/source/Config.py +5 -2
- micrOS/source/Espnow.py +101 -45
- micrOS/source/Files.py +50 -23
- micrOS/source/InterConnect.py +10 -5
- micrOS/source/Pacman.py +141 -0
- micrOS/source/Shell.py +1 -1
- micrOS/source/Tasks.py +59 -54
- micrOS/source/modules/LM_buzzer.py +1 -4
- micrOS/source/modules/LM_cct.py +2 -4
- micrOS/source/modules/LM_dimmer.py +1 -2
- micrOS/source/modules/LM_distance.py +1 -3
- micrOS/source/modules/LM_i2s_mic.py +1 -2
- micrOS/source/modules/LM_keychain.py +1 -2
- micrOS/source/modules/LM_light_sensor.py +1 -4
- micrOS/source/modules/LM_mqtt_client.py +13 -10
- micrOS/source/modules/LM_neopixel.py +1 -2
- micrOS/source/modules/LM_oled_ui.py +39 -41
- micrOS/source/modules/LM_oledui.py +58 -89
- micrOS/source/modules/LM_pacman.py +36 -44
- micrOS/source/modules/LM_presence.py +1 -2
- micrOS/source/modules/LM_rest.py +1 -2
- micrOS/source/modules/LM_rgb.py +1 -2
- micrOS/source/modules/LM_roboarm.py +3 -4
- micrOS/source/modules/LM_robustness.py +1 -2
- micrOS/source/modules/LM_telegram.py +1 -2
- micrOS/source/urequests.py +1 -1
- {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/METADATA +153 -214
- {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/RECORD +67 -67
- toolkit/DevEnvOTA.py +2 -2
- toolkit/DevEnvUSB.py +1 -1
- toolkit/dashboard_apps/SystemTest.py +17 -14
- toolkit/lib/micrOSClient.py +37 -15
- toolkit/lib/micrOSClientHistory.py +35 -1
- toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
- toolkit/simulator_lib/uos.py +1 -0
- toolkit/workspace/precompiled/Common.mpy +0 -0
- toolkit/workspace/precompiled/Config.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/Pacman.mpy +0 -0
- toolkit/workspace/precompiled/Shell.mpy +0 -0
- toolkit/workspace/precompiled/Tasks.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_buzzer.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_cct.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_dimmer.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_distance.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_i2s_mic.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_keychain.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_light_sensor.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_mqtt_client.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_neopixel.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_oled_ui.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_oledui.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_pacman.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_presence.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_rest.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_rgb.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_roboarm.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_robustness.py +1 -2
- toolkit/workspace/precompiled/modules/LM_telegram.mpy +0 -0
- micrOS/source/modules/LM_pet_feeder.py +0 -78
- toolkit/workspace/precompiled/modules/LM_pet_feeder.py +0 -78
- {microsdevtoolkit-2.19.0.data → microsdevtoolkit-2.21.0.data}/scripts/devToolKit.py +0 -0
- {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/WHEEL +0 -0
- {microsdevtoolkit-2.19.0.dist-info → microsdevtoolkit-2.21.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
41
|
+
Check is file/dir protected
|
|
42
|
+
- every file and folder is protected in root dir: /
|
|
43
|
+
- with protected file list
|
|
34
44
|
"""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
113
|
+
def remove_file(path, force=False):
|
|
106
114
|
"""
|
|
107
115
|
Linux like rm command - delete app resources and folders
|
|
108
|
-
:param path:
|
|
109
|
-
:param
|
|
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'
|
|
114
|
-
|
|
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
|
|
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():
|
micrOS/source/InterConnect.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
micrOS/source/Pacman.py
ADDED
|
@@ -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
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
69
|
+
gcollect()
|
|
81
70
|
|
|
71
|
+
###### PUBLIC TASK METHODS #####
|
|
82
72
|
@staticmethod
|
|
83
|
-
|
|
73
|
+
def is_busy(tag:str) -> bool:
|
|
84
74
|
"""
|
|
85
|
-
|
|
86
|
-
:param
|
|
75
|
+
Check task is busy by tag
|
|
76
|
+
:param tag: for task selection
|
|
87
77
|
"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
101
|
+
@staticmethod
|
|
102
|
+
async def feed(sleep_ms=1):
|
|
113
103
|
"""
|
|
114
|
-
|
|
104
|
+
Feed event loop
|
|
105
|
+
:param sleep_ms: in millisecond
|
|
115
106
|
"""
|
|
116
|
-
|
|
117
|
-
if
|
|
118
|
-
|
|
119
|
-
|
|
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) ->
|
|
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.
|
|
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) ->
|
|
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.
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
527
|
+
return True, Manager.create_task(arg_list, loop=loop, delay=delay)
|
|
517
528
|
except Exception as e:
|
|
518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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():
|
micrOS/source/modules/LM_cct.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
#######################
|