micrOSDevToolKit 2.9.1__py3-none-any.whl → 2.9.6__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 (59) hide show
  1. micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +19 -19
  2. micrOS/source/Common.py +1 -1
  3. micrOS/source/Config.py +6 -6
  4. micrOS/source/LM_pacman.py +248 -0
  5. micrOS/source/LM_rest.py +0 -1
  6. micrOS/source/LM_robustness.py +2 -2
  7. micrOS/source/LM_system.py +4 -16
  8. micrOS/source/LM_telegram.py +95 -61
  9. micrOS/source/Logger.py +51 -17
  10. micrOS/source/Notify.py +4 -3
  11. micrOS/source/Server.py +33 -39
  12. micrOS/source/Shell.py +72 -74
  13. micrOS/source/Web.py +22 -23
  14. micrOS/source/microIO.py +1 -2
  15. micrOS/source/reset.py +5 -1
  16. micrOS/source/urequests.py +12 -4
  17. {micrOSDevToolKit-2.9.1.data → micrOSDevToolKit-2.9.6.data}/scripts/devToolKit.py +3 -2
  18. {micrOSDevToolKit-2.9.1.dist-info → micrOSDevToolKit-2.9.6.dist-info}/METADATA +1 -1
  19. {micrOSDevToolKit-2.9.1.dist-info → micrOSDevToolKit-2.9.6.dist-info}/RECORD +57 -54
  20. toolkit/Gateway.py +2 -2
  21. toolkit/dashboard_apps/SystemTest.py +14 -12
  22. toolkit/index.html +4 -4
  23. toolkit/lib/TerminalColors.py +4 -0
  24. toolkit/lib/macroScript.py +6 -0
  25. toolkit/lib/micrOSClient.py +48 -21
  26. toolkit/lib/micrOSClientHistory.py +122 -0
  27. toolkit/micrOSlint.py +1 -1
  28. toolkit/simulator_lib/__pycache__/machine.cpython-312.pyc +0 -0
  29. toolkit/simulator_lib/__pycache__/uasyncio.cpython-312.pyc +0 -0
  30. toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
  31. toolkit/simulator_lib/__pycache__/ussl.cpython-312.pyc +0 -0
  32. toolkit/simulator_lib/machine.py +4 -0
  33. toolkit/simulator_lib/node_config.json +1 -1
  34. toolkit/simulator_lib/uasyncio.py +7 -1
  35. toolkit/simulator_lib/uos.py +138 -0
  36. toolkit/socketClient.py +4 -4
  37. toolkit/workspace/precompiled/Common.mpy +0 -0
  38. toolkit/workspace/precompiled/Config.mpy +0 -0
  39. toolkit/workspace/precompiled/Espnow.mpy +0 -0
  40. toolkit/workspace/precompiled/LM_pacman.py +248 -0
  41. toolkit/workspace/precompiled/LM_rest.mpy +0 -0
  42. toolkit/workspace/precompiled/LM_robustness.py +2 -2
  43. toolkit/workspace/precompiled/LM_system.mpy +0 -0
  44. toolkit/workspace/precompiled/LM_telegram.mpy +0 -0
  45. toolkit/workspace/precompiled/Logger.mpy +0 -0
  46. toolkit/workspace/precompiled/Notify.mpy +0 -0
  47. toolkit/workspace/precompiled/Server.mpy +0 -0
  48. toolkit/workspace/precompiled/Shell.mpy +0 -0
  49. toolkit/workspace/precompiled/Web.mpy +0 -0
  50. toolkit/workspace/precompiled/_mpy.version +1 -1
  51. toolkit/workspace/precompiled/microIO.mpy +0 -0
  52. toolkit/workspace/precompiled/node_config.json +1 -1
  53. toolkit/workspace/precompiled/reset.mpy +0 -0
  54. toolkit/workspace/precompiled/urequests.mpy +0 -0
  55. micrOS/source/LM_lmpacman.py +0 -126
  56. toolkit/workspace/precompiled/LM_lmpacman.mpy +0 -0
  57. {micrOSDevToolKit-2.9.1.dist-info → micrOSDevToolKit-2.9.6.dist-info}/LICENSE +0 -0
  58. {micrOSDevToolKit-2.9.1.dist-info → micrOSDevToolKit-2.9.6.dist-info}/WHEEL +0 -0
  59. {micrOSDevToolKit-2.9.1.dist-info → micrOSDevToolKit-2.9.6.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,4 @@
1
1
  from sys import modules
2
- import uasyncio as asyncio
3
2
  import urequests
4
3
  from Notify import Notify
5
4
  from Config import cfgget
@@ -15,6 +14,10 @@ class Telegram(Notify):
15
14
  _API_PARAMS = "?offset=-1&limit=1&timeout=2" # Generic API params - optimization
16
15
  _IN_MSG_ID = None
17
16
 
17
+ def __init__(self):
18
+ # Subscribe to the notification system - provide send_msg method (over self)
19
+ super().add_subscriber(self)
20
+
18
21
  @staticmethod
19
22
  def __id_cache(mode):
20
23
  """
@@ -97,36 +100,60 @@ class Telegram(Notify):
97
100
  verdict = f'Sent{chat_id}' if resp_json['ok'] else str(resp_json)
98
101
  return verdict
99
102
 
103
+ @staticmethod
104
+ def __update_chat_ids(resp_json):
105
+ """
106
+ Update known chat_id-s and cache them
107
+ - return active chat_id frm resp_json
108
+ """
109
+ console_write("[NTFY GET] update chatIDs")
110
+ _cid = None
111
+ if resp_json.get("ok", None) and len(resp_json["result"]) > 0:
112
+ _cid = resp_json["result"][-1]["message"]["chat"]["id"]
113
+ # LIMIT Telegram._CHAT_IDS NOTIFICATION CACHE TO 3 IDs
114
+ if len(Telegram._CHAT_IDS) < 4:
115
+ _ids = len(Telegram._CHAT_IDS)
116
+ Telegram._CHAT_IDS.add(_cid)
117
+ if len(Telegram._CHAT_IDS) - _ids > 0: # optimized save (slow storage access)
118
+ Telegram.__id_cache('s')
119
+ else:
120
+ Telegram.__id_cache('r')
121
+ if len(Telegram._CHAT_IDS) == 0:
122
+ error_message = resp_json.get("description", "Unknown error")
123
+ raise Exception(f"Error retrieving chat ID: {error_message}")
124
+ return _cid
125
+
100
126
  @staticmethod
101
127
  def get_msg():
102
128
  """
103
129
  Get the last message from the Telegram chat.
104
130
  RETURN None when telegram bot token is missing
105
131
  """
132
+ console_write("[NTFY] GET MESSAGE")
133
+ bot_token = Telegram.__bot_token()
134
+ if bot_token is None:
135
+ return None
136
+ response = {'sender': None, 'text': None, 'm_id': -1, 'c_id': None}
137
+ url = f"https://api.telegram.org/bot{bot_token}/getUpdates{Telegram._API_PARAMS}"
138
+ console_write(f"\t1/2[GET] request: {url}")
106
139
 
107
- def _update_chat_ids():
108
- """
109
- Update known chat_id-s and cache them
110
- - return active chat_id frm resp_json
111
- """
112
- console_write("[NTFY GET] update chatIDs")
113
- _cid = None
114
- if resp_json.get("ok", None) and len(resp_json["result"]) > 0:
115
- _cid = resp_json["result"][-1]["message"]["chat"]["id"]
116
- # LIMIT Telegram._CHAT_IDS NOTIFICATION CACHE TO 3 IDs
117
- if len(Telegram._CHAT_IDS) < 4:
118
- _ids = len(Telegram._CHAT_IDS)
119
- Telegram._CHAT_IDS.add(_cid)
120
- if len(Telegram._CHAT_IDS) - _ids > 0: # optimized save (slow storage access)
121
- Telegram.__id_cache('s')
122
- else:
123
- Telegram.__id_cache('r')
124
- if len(Telegram._CHAT_IDS) == 0:
125
- error_message = resp_json.get("description", "Unknown error")
126
- raise Exception(f"Error retrieving chat ID: {error_message}")
127
- return _cid
140
+ _, resp_json = urequests.get(url, jsonify=True, sock_size=128)
128
141
 
129
- # --------------------- FUNCTION MAIN ------------------------ #
142
+ if len(resp_json["result"]) > 0:
143
+ response['c_id'] = Telegram.__update_chat_ids(resp_json)
144
+ resp = resp_json["result"][-1]["message"]
145
+ response['sender'] = f"{resp['chat']['first_name']}{resp['chat']['last_name']}" if resp['chat'].get(
146
+ 'username', None) is None else resp['chat']['username']
147
+ response['text'], response['m_id'] = resp['text'], resp['message_id']
148
+ console_write(f"\t2/2[GET] response: {response}")
149
+ return response
150
+
151
+ @staticmethod
152
+ async def aget_msg():
153
+ """
154
+ Async: Get the last message from the Telegram chat.
155
+ RETURN None when telegram bot token is missing
156
+ """
130
157
  console_write("[NTFY] GET MESSAGE")
131
158
  bot_token = Telegram.__bot_token()
132
159
  if bot_token is None:
@@ -134,9 +161,11 @@ class Telegram(Notify):
134
161
  response = {'sender': None, 'text': None, 'm_id': -1, 'c_id': None}
135
162
  url = f"https://api.telegram.org/bot{bot_token}/getUpdates{Telegram._API_PARAMS}"
136
163
  console_write(f"\t1/2[GET] request: {url}")
137
- _, resp_json = urequests.get(url, jsonify=True, sock_size=128)
164
+
165
+ _, resp_json = await urequests.aget(url, jsonify=True, sock_size=128)
166
+
138
167
  if len(resp_json["result"]) > 0:
139
- response['c_id'] = _update_chat_ids()
168
+ response['c_id'] = Telegram.__update_chat_ids(resp_json)
140
169
  resp = resp_json["result"][-1]["message"]
141
170
  response['sender'] = f"{resp['chat']['first_name']}{resp['chat']['last_name']}" if resp['chat'].get(
142
171
  'username', None) is None else resp['chat']['username']
@@ -145,16 +174,13 @@ class Telegram(Notify):
145
174
  return response
146
175
 
147
176
  @staticmethod
148
- def receive_eval():
177
+ async def receive_eval():
149
178
  """
150
179
  READ - VALIDATE - EXECUTE - REPLY LOOP
151
180
  - can be used in async loop
152
181
  RETURN None when telegram bot token is missing
153
182
  """
154
-
155
- console_write("[NTFY] REC&EVAL sequence")
156
-
157
- # Return data structure template
183
+ console_write("[NTFY] EVAL sequence")
158
184
  verdict = None
159
185
 
160
186
  def lm_execute(cmd_args):
@@ -168,8 +194,8 @@ class Telegram(Notify):
168
194
  Telegram._IN_MSG_ID = m_id
169
195
 
170
196
  # -------------------------- FUNCTION MAIN -------------------------- #
171
- # Poll telegram chat
172
- data = Telegram.get_msg()
197
+ # Async Poll telegram chat
198
+ data = await Telegram.aget_msg()
173
199
  if data is None:
174
200
  return data
175
201
  # Get msg, msg_id, chat_id as main input data source
@@ -197,12 +223,42 @@ class Telegram(Notify):
197
223
  verdict = Telegram.notifications(not param[0].strip().lower() in ("disable", "off", 'false'))
198
224
  else:
199
225
  verdict = Telegram.notifications()
226
+ # Send is still synchronous (OK)
200
227
  Telegram.send_msg(verdict, reply_to=m_id)
201
228
  else:
202
229
  verdict = "[UP] NoExec"
203
- console_write(f"\tREC&EVAL: {verdict}")
230
+ console_write(f"\tBOT: {verdict}")
204
231
  return verdict
205
232
 
233
+ @staticmethod
234
+ async def bot_repl(tag, period=3):
235
+ """
236
+ BOT - ReceiveEvalPrintLoop
237
+ :param tag: task tag (access)
238
+ :param period: polling period in sec, default: 3
239
+ """
240
+ cancel_cnt = 0
241
+ period = period if period > 0 else 1
242
+ period_ms = period * 1000
243
+ with micro_task(tag=tag) as my_task:
244
+ my_task.out = "[UP] Running"
245
+ while True:
246
+ # Normal task period
247
+ await my_task.feed(sleep_ms=period_ms)
248
+ try:
249
+ v = await Telegram.receive_eval()
250
+ my_task.out = "Missing bot token" if v is None else f"{v} ({period}s)"
251
+ cancel_cnt = 0
252
+ except Exception as e:
253
+ my_task.out = str(e)
254
+ # Auto scale - blocking nature - in case of serial failures (5) - hibernate task (increase async sleep)
255
+ cancel_cnt += 1
256
+ if cancel_cnt > 5:
257
+ my_task.out = f"[DOWN] {e} (wait 1min)"
258
+ cancel_cnt = 5
259
+ # SLOW DOWN - hibernate task
260
+ await my_task.feed(sleep_ms=60_000)
261
+
206
262
  @staticmethod
207
263
  def set_commands():
208
264
  """
@@ -237,7 +293,6 @@ def __init():
237
293
  _sta_available = True if ifconfig()[0] == "STA" else False
238
294
  if _sta_available:
239
295
  TELEGRAM_OBJ = Telegram()
240
- Notify.add_subscriber(TELEGRAM_OBJ)
241
296
  else:
242
297
  syslog("No STA: cannot init telegram")
243
298
 
@@ -292,37 +347,16 @@ def receive():
292
347
  return "Missing telegram bot token" if verdict is None else verdict
293
348
 
294
349
 
295
- async def __task():
296
- cancel_cnt = 0
297
- with micro_task(tag='telegram._loop') as my_task:
298
- my_task.out = "[UP] Running"
299
- while True:
300
- # Normal task period
301
- await asyncio.sleep(5)
302
- try:
303
- v = TELEGRAM_OBJ.receive_eval()
304
- my_task.out = "Missing bot token" if v is None else v
305
- cancel_cnt = 0
306
- except Exception as e:
307
- my_task.out = str(e)
308
- # Auto scale - blocking nature - in case of serial failures (5) - hibernate task (increase async sleep)
309
- cancel_cnt += 1
310
- if cancel_cnt > 5:
311
- my_task.out = f"[DOWN] {e}"
312
- cancel_cnt = 5
313
- # SLOW DOWN - hibernate task
314
- asyncio.sleep(115)
315
-
316
-
317
- def receiver_loop():
350
+ def receiver_loop(period=3):
318
351
  """
319
- Telegram msg receiver loop - automatic LM execution
352
+ Telegram BOT (repl) - ReadEvalPrintLoop for Load Modules
320
353
  - Only executes module (function) if the module is already loaded
321
- on the endpoint / micrOS node
354
+ :param period: polling period in sec, default: 3
322
355
  """
323
356
  if TELEGRAM_OBJ is None:
324
357
  return "Network unavailable."
325
- state = micro_task(tag='telegram._loop', task=__task())
358
+ tag = 'telegram.bot_repl'
359
+ state = micro_task(tag=tag, task=TELEGRAM_OBJ.bot_repl(tag=tag, period=period))
326
360
  return "Starting" if state else "Already running"
327
361
 
328
362
 
@@ -335,6 +369,6 @@ def help(widgets=False):
335
369
  """
336
370
  return ('send "text"',
337
371
  'receive',
338
- 'receiver_loop',
372
+ 'receiver_loop period=3',
339
373
  'notify "message"',
340
374
  'load', 'INFO: Send & Receive messages with Telegram bot')
micrOS/source/Logger.py CHANGED
@@ -5,11 +5,34 @@ Module is responsible for System and User logging
5
5
  Designed by Marcell Ban aka BxNxM
6
6
  """
7
7
  from time import localtime
8
- from os import listdir, remove
8
+ from re import match
9
+ from uos import listdir, remove, stat, mkdir, getcwd
9
10
 
10
11
  #############################################
11
12
  # LOGGING WITH DATA ROTATION #
12
13
  #############################################
14
+ LOG_FOLDER = None
15
+
16
+ def _init_logger():
17
+ """ Init /logs folder """
18
+ global LOG_FOLDER
19
+ if LOG_FOLDER is None:
20
+ LOG_FOLDER = f"{getcwd()}logs"
21
+ do_create = True
22
+ try:
23
+ if stat(LOG_FOLDER)[0] & 0x4000:
24
+ # Dir exists - skip create
25
+ do_create = False
26
+ except:
27
+ pass
28
+ if do_create:
29
+ try:
30
+ mkdir(LOG_FOLDER)
31
+ syslog(f"[BOOT] log dir {LOG_FOLDER} init")
32
+ except Exception as e:
33
+ LOG_FOLDER = getcwd()
34
+ syslog(f"[BOOT] log dir {LOG_FOLDER} fallback: {e}")
35
+ return LOG_FOLDER
13
36
 
14
37
 
15
38
  def logger(data, f_name, limit):
@@ -23,14 +46,15 @@ def logger(data, f_name, limit):
23
46
  return write verdict - true / false
24
47
  INFO: hardcoded max data number = 30
25
48
  """
26
- def _logger(_data, _f_name, _limit, f_mode='r+'):
27
- _limit = 30 if _limit > 30 else _limit
49
+ def _logger(f_mode='r+'):
50
+ nonlocal data, f_name, limit
51
+ limit = min(limit, 30) # Hardcoded max data line = 30
28
52
  # [1] GET TIME STUMP
29
53
  ts_buff = [str(k) for k in localtime()]
30
54
  ts = ".".join(ts_buff[0:3]) + "-" + ":".join(ts_buff[3:6])
31
55
  # [2] OPEN FILE - WRITE DATA WITH TS
32
- with open(_f_name, f_mode) as f:
33
- _data = f"{ts} {_data}\n"
56
+ with open(f_name, f_mode) as f:
57
+ _data = f"{ts} {data}\n"
34
58
  # read file lines and filter by time stump chunks (hack for replace truncate)
35
59
  lines = [_l for _l in f.readlines() if '-' in _l and '.' in _l]
36
60
  # get file params
@@ -38,22 +62,20 @@ def logger(data, f_name, limit):
38
62
  lines.append(_data)
39
63
  f.seek(0)
40
64
  # line data rotate
41
- if lines_len >= _limit:
42
- lines = lines[-_limit:]
43
- lines_str = ''.join(lines)
44
- else:
45
- lines_str = ''.join(lines)
65
+ if lines_len >= limit:
66
+ lines = lines[-limit:]
46
67
  # write file
47
- f.write(lines_str)
68
+ f.write(''.join(lines))
48
69
 
70
+ f_name = f"{LOG_FOLDER}/{f_name}"
49
71
  # Run logger
50
72
  try:
51
73
  # There is file - append 'r+'
52
- _logger(data, f_name, limit)
74
+ _logger()
53
75
  except:
54
76
  try:
55
77
  # There is no file - create 'a+'
56
- _logger(data, f_name, limit, 'a+')
78
+ _logger('a+')
57
79
  except:
58
80
  return False
59
81
  return True
@@ -64,8 +86,11 @@ def log_get(f_name, msgobj=None):
64
86
  Get and stream (ver osocket/stdout) .log file's content and count "critical" errors
65
87
  - critical error tag in log line: [ERR]
66
88
  """
89
+ f_name = f"{LOG_FOLDER}/{f_name}"
67
90
  err_cnt = 0
68
91
  try:
92
+ if msgobj is not None:
93
+ msgobj(f_name)
69
94
  with open(f_name, 'r') as f:
70
95
  eline = f.readline().strip()
71
96
  while eline:
@@ -73,7 +98,7 @@ def log_get(f_name, msgobj=None):
73
98
  err_cnt += 1 if "[ERR]" in eline else 0
74
99
  # GIVE BACK .log file contents
75
100
  if msgobj is not None:
76
- msgobj(eline)
101
+ msgobj(f"\t{eline}")
77
102
  eline = f.readline().strip()
78
103
  except:
79
104
  pass
@@ -82,13 +107,22 @@ def log_get(f_name, msgobj=None):
82
107
 
83
108
  def syslog(data=None, msgobj=None):
84
109
  if data is None:
85
- return log_get('err.log', msgobj)
86
- return logger(data, 'err.log', limit=6)
110
+ err_cnt = sum([log_get(f, msgobj) for f in listdir(LOG_FOLDER) if f.endswith(".sys.log")])
111
+ return err_cnt
112
+
113
+ _match = match(r"^\[([^\[\]]+)\]", data)
114
+ log_lvl = _match.group(1).lower() if _match else 'user'
115
+ f_name = f"{log_lvl}.sys.log" if log_lvl in ("err", "warn", "boot") else 'user.sys.log'
116
+ return logger(data, f_name, limit=4)
87
117
 
88
118
 
89
119
  def log_clean(msgobj=None):
90
- to_del = [file for file in listdir() if file.endswith('.log')]
120
+ to_del = [file for file in listdir(LOG_FOLDER) if file.endswith('.log')]
91
121
  for _del in to_del:
122
+ _del = f"{LOG_FOLDER}/{_del}"
92
123
  if msgobj is not None:
93
124
  msgobj(f" Delete: {_del}")
94
125
  remove(_del)
126
+
127
+ # Init log folder at module load
128
+ _init_logger()
micrOS/source/Notify.py CHANGED
@@ -35,16 +35,17 @@ class Notify:
35
35
  @staticmethod
36
36
  def message(text, reply_to=None, chat_id=None):
37
37
  """
38
- Send message to all subscribers
38
+ Send message to all subscribers - Notify agents
39
39
  """
40
40
  exit_code = 0
41
41
  for s in Notify._SUBSCRIBERS:
42
42
  try:
43
+ # !!! SUBSCRIBER HAS TO DEFINE send_msg(text, reply_to, chat_id) method !!!
43
44
  s.send_msg(text, reply_to, chat_id)
44
45
  except Exception as e:
45
46
  errlog_add(f"[ERR] Notify: {e}")
46
47
  exit_code+=1
47
- return f"Sent N{len(Notify._SUBSCRIBERS)} ({exit_code})"
48
+ return f"Sent for {len(Notify._SUBSCRIBERS)} client(s), errors: ({exit_code})"
48
49
 
49
50
  @staticmethod
50
51
  def notifications(state=None):
@@ -66,7 +67,7 @@ class Notify:
66
67
 
67
68
  @staticmethod
68
69
  def lm_execute(cmd_args):
69
- """Executor with basic access handling"""
70
+ """Load Module Executor with basic access handling"""
70
71
  if lm_is_loaded(cmd_args[0]):
71
72
  try:
72
73
  _, out = lm_exec(cmd_args)
micrOS/source/Server.py CHANGED
@@ -114,7 +114,7 @@ class Client:
114
114
  console_write("[Client] NoCon: response>dev/nul")
115
115
 
116
116
  def send(self, response):
117
- # Implement in child class - synchronous send method
117
+ # Implement in child class - synchronous send (all) method
118
118
  pass
119
119
 
120
120
  async def close(self):
@@ -182,7 +182,7 @@ class WebCli(Client, WebEngine):
182
182
  if cfgput('webui', True): # SET webui to True
183
183
  from machine import reset
184
184
  reset() # HARD RESET (REBOOT)
185
- WebEngine.REST_ENDPOINTS[endpoint] = callback
185
+ WebEngine.ENDPOINTS[endpoint] = callback
186
186
 
187
187
 
188
188
  class ShellCli(Client, Shell):
@@ -196,7 +196,9 @@ class ShellCli(Client, Shell):
196
196
 
197
197
  def send(self, response):
198
198
  """
199
- Send response to client with non-async function
199
+ Synchronous send function (with drain task)
200
+ - not used in Shell or ShellCli
201
+ - Note: it is a support function for synchronous scenarios: Server.reply_all
200
202
  """
201
203
  if self.connected:
202
204
  if self.prompt() != response:
@@ -219,8 +221,7 @@ class ShellCli(Client, Shell):
219
221
 
220
222
  async def __wait_for_drain(self):
221
223
  """
222
- Handle drain serialization
223
- - solve output data duplicate
224
+ Handle drain serialization - for synchronous send function
224
225
  """
225
226
  try:
226
227
  # send write buffer
@@ -233,55 +234,45 @@ class ShellCli(Client, Shell):
233
234
  # set drain free
234
235
  self.drain_event.set() # set drain free (True)
235
236
 
236
- async def close(self):
237
- Client.console(f"[ShellCli] Close connection {self.client_id}")
238
- self.send("Bye!\n")
239
- # Reset shell state machine
240
- self.reset()
241
- await asyncio.sleep_ms(50)
242
- # Used from Client parent class
243
- await super().close()
244
-
245
- async def __shell_cmd(self, request):
237
+ async def a_send(self, response, encode='utf8'):
246
238
  """
247
- Handle micrOS shell commands
239
+ Async send for Shell (new line + prompt$)
248
240
  """
249
- # Run micrOS shell with request string
250
- try:
251
- # Handle micrOS shell
252
- Client.console("[ShellCli] --- #Run shell")
253
- state = self.shell(request)
254
- if state:
255
- return False # exit_loop
256
- return True # exit_loop : Close session when shell returns False (auth Failed, etc.)
257
- except Exception as e:
258
- Client.console(f"[ShellCli] Shell exception: {e}")
259
- if "ECONNRESET" in str(e):
260
- return True # exit_loop
261
- self.send("[HA] Critical error - disconnect & hard reset")
262
- errlog_add("[ERR] Socket critical error - reboot")
263
- self.reboot()
241
+ if self.prompt() != response:
242
+ # [format] Add new line if not prompt
243
+ response = f"{response}\n"
244
+ await super().a_send(response, encode)
264
245
 
265
246
  async def run_shell(self):
266
247
  # Update server task output
267
248
  Manager().server_task_msg(','.join(list(Client.ACTIVE_CLIS)))
268
249
  # Init prompt
269
- self.send(self.prompt())
250
+ await self.a_send(self.prompt())
270
251
  # Run async connection handling
252
+ _exit = False
271
253
  while self.connected:
272
254
  try:
273
255
  # Read request msg from client
274
256
  state, request = await self.read()
275
257
  if state:
276
258
  break
277
- _exit = await self.__shell_cmd(request)
278
- if _exit:
279
- collect()
280
- break
259
+ # Run micrOS shell with request string
260
+ Client.console("[ShellCli] --- #Run shell")
261
+ # Shell -> True (OK) or False (NOK) -> NOK->Close session (auth Failed, etc.)
262
+ _exit = not await self.shell(request)
281
263
  except Exception as e:
282
- errlog_add(f"[ERR] handle_client: {e}")
264
+ errlog_add(f"[ERR] Shell client: {e}")
265
+ if "ECONNRESET" in str(e):
266
+ _exit = True # exit_loop
267
+ else:
268
+ await self.a_send("[HA] Critical error - disconnect & hard reset")
269
+ errlog_add("[ERR] Socket critical error - reboot")
270
+ await self.reboot()
271
+ if _exit:
272
+ collect()
283
273
  break
284
274
  # Close connection
275
+ await self.a_send("Bye!")
285
276
  await self.close()
286
277
 
287
278
 
@@ -418,8 +409,11 @@ class Server:
418
409
  """
419
410
  for _, cli in Client.ACTIVE_CLIS.items():
420
411
  if cli.connected:
421
- cli.send(msg)
412
+ cli.send(f"~~~ {msg}")
422
413
 
423
414
  def __del__(self):
424
415
  Client.console("[ socket server ] <<destructor>>")
425
- self.server.close()
416
+ if self.server:
417
+ self.server.close()
418
+ if self.web:
419
+ self.web.close()