micrOSDevToolKit 2.9.1__py3-none-any.whl → 2.26.1__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.
Files changed (368) hide show
  1. env/driver_cp210x/macOS_VCP_Driver/SiLabsUSBDriverDisk.dmg +0 -0
  2. env/driver_cp210x/macOS_VCP_Driver/macOS_VCP_Driver_Release_Notes.txt +17 -1
  3. micrOS/micropython/esp32-20251209-v1.27.0.bin +0 -0
  4. micrOS/micropython/esp32c3-20251209-v1.27.0.bin +0 -0
  5. micrOS/micropython/esp32c6-20251209-v1.27.0.bin +0 -0
  6. micrOS/micropython/esp32s2-20251209-v1.27.0.bin +0 -0
  7. micrOS/micropython/esp32s2-LOLIN_MINI-20251209-v1.27.0.bin +0 -0
  8. micrOS/micropython/{esp32s3-20241129-v1.24.1.bin → esp32s3-4MBflash-20241129-v1.24.1.bin} +0 -0
  9. micrOS/micropython/esp32s3-8MBflash-20251209-v1.27.0.bin +0 -0
  10. micrOS/micropython/esp32s3_spiram_oct-20251209-v1.27.0.bin +0 -0
  11. micrOS/micropython/rpi-pico-w-20251209-v1.27.0.uf2 +0 -0
  12. micrOS/micropython/tinypico-20251209-v1.27.0.bin +0 -0
  13. micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +167 -163
  14. micrOS/source/Auth.py +37 -0
  15. micrOS/source/Common.py +361 -116
  16. micrOS/source/Config.py +32 -22
  17. micrOS/source/Debug.py +50 -94
  18. micrOS/source/Espnow.py +377 -100
  19. micrOS/source/Files.py +207 -0
  20. micrOS/source/Hooks.py +48 -20
  21. micrOS/source/InterConnect.py +126 -42
  22. micrOS/source/Interrupts.py +6 -6
  23. micrOS/source/Logger.py +63 -26
  24. micrOS/source/Network.py +41 -21
  25. micrOS/source/Notify.py +48 -22
  26. micrOS/source/Pacman.py +326 -0
  27. micrOS/source/Scheduler.py +14 -54
  28. micrOS/source/Server.py +67 -69
  29. micrOS/source/Shell.py +99 -91
  30. micrOS/source/Tasks.py +141 -95
  31. micrOS/source/Time.py +19 -18
  32. micrOS/source/Types.py +53 -9
  33. micrOS/source/Web.py +381 -76
  34. micrOS/source/__pycache__/Common.cpython-312.pyc +0 -0
  35. micrOS/source/__pycache__/Debug.cpython-312.pyc +0 -0
  36. micrOS/source/__pycache__/Files.cpython-312.pyc +0 -0
  37. micrOS/source/__pycache__/Logger.cpython-312.pyc +0 -0
  38. micrOS/source/__pycache__/Scheduler.cpython-312.pyc +0 -0
  39. micrOS/source/__pycache__/Server.cpython-312.pyc +0 -0
  40. micrOS/source/__pycache__/Shell.cpython-312.pyc +0 -0
  41. micrOS/source/__pycache__/replhelper.cpython-312.pyc +0 -0
  42. micrOS/source/config/_git.keep +0 -0
  43. micrOS/source/helpers.py +132 -0
  44. micrOS/source/micrOS.py +17 -7
  45. micrOS/source/micrOSloader.py +5 -12
  46. micrOS/source/microIO.py +44 -20
  47. micrOS/source/modules/IO_esp32c6.py +38 -0
  48. micrOS/source/{IO_esp32s3.py → modules/IO_esp32s3.py} +37 -1
  49. micrOS/source/{IO_m5stamp.py → modules/IO_m5stamp.py} +35 -1
  50. micrOS/source/{IO_qtpy.py → modules/IO_qtpy.py} +22 -17
  51. micrOS/source/{IO_tinypico.py → modules/IO_tinypico.py} +38 -0
  52. micrOS/source/modules/LM_L298N.py +161 -0
  53. {toolkit/workspace/precompiled → micrOS/source/modules}/LM_L9110_DCmotor.py +3 -3
  54. micrOS/source/{LM_OV2640.py → modules/LM_OV2640.py} +45 -27
  55. micrOS/source/{LM_VL53L0X.py → modules/LM_VL53L0X.py} +3 -3
  56. micrOS/source/{LM_aht10.py → modules/LM_aht10.py} +2 -2
  57. micrOS/source/{LM_bme280.py → modules/LM_bme280.py} +3 -3
  58. micrOS/source/{LM_buzzer.py → modules/LM_buzzer.py} +18 -25
  59. micrOS/source/{LM_cct.py → modules/LM_cct.py} +17 -21
  60. micrOS/source/modules/LM_cluster.py +255 -0
  61. micrOS/source/{LM_co2.py → modules/LM_co2.py} +3 -3
  62. micrOS/source/{LM_dht11.py → modules/LM_dht11.py} +2 -2
  63. micrOS/source/{LM_dht22.py → modules/LM_dht22.py} +2 -2
  64. micrOS/source/{LM_dimmer.py → modules/LM_dimmer.py} +9 -9
  65. micrOS/source/{LM_distance.py → modules/LM_distance.py} +4 -6
  66. micrOS/source/{LM_ds18.py → modules/LM_ds18.py} +2 -2
  67. micrOS/source/{LM_esp32.py → modules/LM_esp32.py} +5 -0
  68. micrOS/source/modules/LM_espnow.py +53 -0
  69. micrOS/source/modules/LM_fileserver.py +265 -0
  70. micrOS/source/{LM_genIO.py → modules/LM_genIO.py} +52 -37
  71. micrOS/source/{LM_haptic.py → modules/LM_haptic.py} +2 -2
  72. {toolkit/workspace/precompiled → micrOS/source/modules}/LM_i2c.py +5 -4
  73. micrOS/source/{LM_i2s_mic.py → modules/LM_i2s_mic.py} +6 -7
  74. micrOS/source/{LM_ld2410.py → modules/LM_ld2410.py} +2 -2
  75. micrOS/source/{LM_light_sensor.py → modules/LM_light_sensor.py} +10 -21
  76. micrOS/source/modules/LM_mh_z19c.py +198 -0
  77. micrOS/source/modules/LM_neoeffects.py +284 -0
  78. micrOS/source/{LM_neopixel.py → modules/LM_neopixel.py} +19 -23
  79. micrOS/source/{LM_oled.py → modules/LM_oled.py} +2 -2
  80. micrOS/source/{LM_oled_sh1106.py → modules/LM_oled_sh1106.py} +3 -3
  81. micrOS/source/{LM_oled_ui.py → modules/LM_oled_ui.py} +72 -64
  82. micrOS/source/modules/LM_pacman.py +320 -0
  83. micrOS/source/{LM_presence.py → modules/LM_presence.py} +11 -15
  84. micrOS/source/modules/LM_qmi8658.py +204 -0
  85. micrOS/source/{LM_rencoder.py → modules/LM_rencoder.py} +2 -2
  86. micrOS/source/{LM_rest.py → modules/LM_rest.py} +4 -6
  87. micrOS/source/{LM_rgb.py → modules/LM_rgb.py} +21 -29
  88. micrOS/source/{LM_roboarm.py → modules/LM_roboarm.py} +8 -8
  89. micrOS/source/modules/LM_robustness.py +137 -0
  90. micrOS/source/{LM_servo.py → modules/LM_servo.py} +3 -3
  91. micrOS/source/{LM_stepper.py → modules/LM_stepper.py} +5 -5
  92. micrOS/source/{LM_switch.py → modules/LM_switch.py} +11 -9
  93. micrOS/source/{LM_system.py → modules/LM_system.py} +38 -32
  94. micrOS/source/modules/LM_tcs3472.py +187 -0
  95. micrOS/source/{LM_telegram.py → modules/LM_telegram.py} +164 -116
  96. micrOS/source/{LM_trackball.py → modules/LM_trackball.py} +3 -3
  97. micrOS/source/{LM_veml7700.py → modules/LM_veml7700.py} +2 -2
  98. micrOS/source/modules/LM_web.py +38 -0
  99. micrOS/source/urequests.py +39 -15
  100. {toolkit/workspace/precompiled → micrOS/source/web}/dashboard.html +4 -0
  101. micrOS/source/web/editor.js +440 -0
  102. micrOS/source/web/filesui.html +178 -0
  103. micrOS/source/web/filesui.js +338 -0
  104. {toolkit/workspace/precompiled → micrOS/source/web}/index.html +44 -2
  105. micrOS/source/{uapi.js → web/uapi.js} +48 -7
  106. micrOS/source/{ustyle.css → web/ustyle.css} +6 -3
  107. micrOS/utests/__init__.py +0 -0
  108. micrOS/utests/test_scheduler.py +435 -0
  109. {micrOSDevToolKit-2.9.1.data → microsdevtoolkit-2.26.1.data}/scripts/devToolKit.py +33 -3
  110. {micrOSDevToolKit-2.9.1.dist-info → microsdevtoolkit-2.26.1.dist-info}/METADATA +327 -268
  111. microsdevtoolkit-2.26.1.dist-info/RECORD +396 -0
  112. {micrOSDevToolKit-2.9.1.dist-info → microsdevtoolkit-2.26.1.dist-info}/WHEEL +1 -1
  113. toolkit/DevEnvCompile.py +63 -33
  114. toolkit/DevEnvOTA.py +58 -22
  115. toolkit/DevEnvUSB.py +110 -55
  116. toolkit/Gateway.py +6 -6
  117. toolkit/LM_to_compile.dat +6 -4
  118. toolkit/MicrOSDevEnv.py +127 -57
  119. toolkit/WebRepl.py +73 -0
  120. toolkit/dashboard_apps/BackupRestore.py +20 -35
  121. toolkit/dashboard_apps/CCTDemo.py +12 -17
  122. toolkit/dashboard_apps/CCTTest.py +20 -24
  123. toolkit/dashboard_apps/CamStream.py +2 -6
  124. toolkit/dashboard_apps/CatGame.py +14 -16
  125. toolkit/dashboard_apps/Dimmer.py +11 -21
  126. toolkit/dashboard_apps/GetVersion.py +11 -19
  127. toolkit/dashboard_apps/MicrophoneTest.py +1 -6
  128. toolkit/dashboard_apps/NeoEffectsDemo.py +22 -35
  129. toolkit/dashboard_apps/NeopixelTest.py +20 -25
  130. toolkit/dashboard_apps/PresenceTest.py +2 -8
  131. toolkit/dashboard_apps/QMI8685_GYRO.py +68 -0
  132. toolkit/dashboard_apps/RGBTest.py +20 -24
  133. toolkit/dashboard_apps/RoboArm.py +24 -32
  134. toolkit/dashboard_apps/SED_test.py +10 -14
  135. toolkit/dashboard_apps/SensorsTest.py +61 -0
  136. toolkit/dashboard_apps/SystemTest.py +202 -105
  137. toolkit/dashboard_apps/Template_app.py +11 -23
  138. toolkit/dashboard_apps/_app_base.py +34 -0
  139. toolkit/dashboard_apps/_gyro_visualizer.py +78 -0
  140. toolkit/dashboard_apps/uLightDemo.py +15 -24
  141. toolkit/index.html +4 -4
  142. toolkit/lib/LocalMachine.py +6 -1
  143. toolkit/lib/MicrosFiles.py +46 -0
  144. toolkit/lib/Repository.py +64 -0
  145. toolkit/lib/TerminalColors.py +4 -0
  146. toolkit/lib/macroScript.py +6 -0
  147. toolkit/lib/micrOSClient.py +123 -50
  148. toolkit/lib/micrOSClientHistory.py +156 -0
  149. toolkit/lib/pip_package_installer.py +5 -2
  150. toolkit/micrOSdashboard.py +12 -17
  151. toolkit/micrOSlint.py +20 -8
  152. toolkit/simulator_lib/__pycache__/IO_darwin.cpython-312.pyc +0 -0
  153. toolkit/simulator_lib/__pycache__/aioespnow.cpython-312.pyc +0 -0
  154. toolkit/simulator_lib/__pycache__/framebuf.cpython-312.pyc +0 -0
  155. toolkit/simulator_lib/__pycache__/machine.cpython-312.pyc +0 -0
  156. toolkit/simulator_lib/__pycache__/micropython.cpython-312.pyc +0 -0
  157. toolkit/simulator_lib/__pycache__/mip.cpython-312.pyc +0 -0
  158. toolkit/simulator_lib/__pycache__/neopixel.cpython-312.pyc +0 -0
  159. toolkit/simulator_lib/__pycache__/network.cpython-312.pyc +0 -0
  160. toolkit/simulator_lib/__pycache__/sim_common.cpython-312.pyc +0 -0
  161. toolkit/simulator_lib/__pycache__/simgc.cpython-312.pyc +0 -0
  162. toolkit/simulator_lib/__pycache__/simulator.cpython-312.pyc +0 -0
  163. toolkit/simulator_lib/__pycache__/uasyncio.cpython-312.pyc +0 -0
  164. toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
  165. toolkit/simulator_lib/__pycache__/urandom.cpython-312.pyc +0 -0
  166. toolkit/simulator_lib/__pycache__/usocket.cpython-312.pyc +0 -0
  167. toolkit/simulator_lib/__pycache__/ussl.cpython-312.pyc +0 -0
  168. toolkit/simulator_lib/aioespnow.py +28 -0
  169. toolkit/simulator_lib/dht.py +1 -1
  170. toolkit/simulator_lib/framebuf.py +49 -1
  171. toolkit/simulator_lib/machine.py +17 -2
  172. toolkit/simulator_lib/micropython.py +3 -3
  173. toolkit/simulator_lib/mip.py +165 -0
  174. toolkit/simulator_lib/neopixel.py +3 -2
  175. toolkit/simulator_lib/network.py +2 -1
  176. toolkit/simulator_lib/node_config.json +2 -3
  177. toolkit/simulator_lib/ntptime.py +1 -1
  178. toolkit/simulator_lib/{sim_console.py → sim_common.py} +2 -3
  179. toolkit/simulator_lib/simgc.py +6 -2
  180. toolkit/simulator_lib/simulator.py +137 -59
  181. toolkit/simulator_lib/uasyncio.py +33 -2
  182. toolkit/simulator_lib/uos.py +147 -0
  183. toolkit/simulator_lib/urandom.py +4 -0
  184. toolkit/socketClient.py +43 -23
  185. toolkit/user_data/webhooks/generic.py +1 -1
  186. toolkit/user_data/webhooks/macro.py +1 -1
  187. toolkit/user_data/webhooks/template.py +1 -1
  188. toolkit/workspace/precompiled/Auth.mpy +0 -0
  189. toolkit/workspace/precompiled/Common.mpy +0 -0
  190. toolkit/workspace/precompiled/Config.mpy +0 -0
  191. toolkit/workspace/precompiled/Debug.mpy +0 -0
  192. toolkit/workspace/precompiled/Espnow.mpy +0 -0
  193. toolkit/workspace/precompiled/Files.mpy +0 -0
  194. toolkit/workspace/precompiled/Hooks.mpy +0 -0
  195. toolkit/workspace/precompiled/InterConnect.mpy +0 -0
  196. toolkit/workspace/precompiled/Interrupts.mpy +0 -0
  197. toolkit/workspace/precompiled/Logger.mpy +0 -0
  198. toolkit/workspace/precompiled/Network.mpy +0 -0
  199. toolkit/workspace/precompiled/Notify.mpy +0 -0
  200. toolkit/workspace/precompiled/Pacman.mpy +0 -0
  201. toolkit/workspace/precompiled/Scheduler.mpy +0 -0
  202. toolkit/workspace/precompiled/Server.mpy +0 -0
  203. toolkit/workspace/precompiled/Shell.mpy +0 -0
  204. toolkit/workspace/precompiled/Tasks.mpy +0 -0
  205. toolkit/workspace/precompiled/Time.mpy +0 -0
  206. toolkit/workspace/precompiled/Types.mpy +0 -0
  207. toolkit/workspace/precompiled/Web.mpy +0 -0
  208. toolkit/workspace/precompiled/_mpy.version +1 -1
  209. toolkit/workspace/precompiled/config/_git.keep +0 -0
  210. toolkit/workspace/precompiled/helpers.mpy +0 -0
  211. toolkit/workspace/precompiled/micrOS.mpy +0 -0
  212. toolkit/workspace/precompiled/micrOSloader.mpy +0 -0
  213. toolkit/workspace/precompiled/microIO.mpy +0 -0
  214. toolkit/workspace/precompiled/{IO_esp32.mpy → modules/IO_esp32.mpy} +0 -0
  215. toolkit/workspace/precompiled/{IO_esp32c3.mpy → modules/IO_esp32c3.mpy} +0 -0
  216. toolkit/workspace/precompiled/modules/IO_esp32c6.mpy +0 -0
  217. toolkit/workspace/precompiled/{IO_esp32s2.mpy → modules/IO_esp32s2.mpy} +0 -0
  218. toolkit/workspace/precompiled/modules/IO_esp32s3.mpy +0 -0
  219. toolkit/workspace/precompiled/modules/IO_m5stamp.mpy +0 -0
  220. toolkit/workspace/precompiled/modules/IO_qtpy.mpy +0 -0
  221. toolkit/workspace/precompiled/modules/IO_rp2.mpy +0 -0
  222. toolkit/workspace/precompiled/modules/IO_tinypico.mpy +0 -0
  223. toolkit/workspace/precompiled/modules/LM_L298N.mpy +0 -0
  224. {micrOS/source → toolkit/workspace/precompiled/modules}/LM_L9110_DCmotor.py +3 -3
  225. toolkit/workspace/precompiled/modules/LM_OV2640.mpy +0 -0
  226. toolkit/workspace/precompiled/{LM_VL53L0X.py → modules/LM_VL53L0X.py} +3 -3
  227. toolkit/workspace/precompiled/{LM_aht10.mpy → modules/LM_aht10.mpy} +0 -0
  228. toolkit/workspace/precompiled/{LM_bme280.mpy → modules/LM_bme280.mpy} +0 -0
  229. toolkit/workspace/precompiled/{LM_buzzer.mpy → modules/LM_buzzer.mpy} +0 -0
  230. toolkit/workspace/precompiled/modules/LM_cct.mpy +0 -0
  231. toolkit/workspace/precompiled/modules/LM_cluster.mpy +0 -0
  232. toolkit/workspace/precompiled/{LM_co2.mpy → modules/LM_co2.mpy} +0 -0
  233. toolkit/workspace/precompiled/{LM_dht11.mpy → modules/LM_dht11.mpy} +0 -0
  234. toolkit/workspace/precompiled/{LM_dht22.mpy → modules/LM_dht22.mpy} +0 -0
  235. toolkit/workspace/precompiled/modules/LM_dimmer.mpy +0 -0
  236. toolkit/workspace/precompiled/modules/LM_distance.mpy +0 -0
  237. toolkit/workspace/precompiled/{LM_ds18.mpy → modules/LM_ds18.mpy} +0 -0
  238. toolkit/workspace/precompiled/{LM_esp32.py → modules/LM_esp32.py} +5 -0
  239. toolkit/workspace/precompiled/modules/LM_espnow.py +53 -0
  240. toolkit/workspace/precompiled/modules/LM_fileserver.mpy +0 -0
  241. toolkit/workspace/precompiled/{LM_gameOfLife.mpy → modules/LM_gameOfLife.mpy} +0 -0
  242. toolkit/workspace/precompiled/modules/LM_genIO.mpy +0 -0
  243. toolkit/workspace/precompiled/{LM_haptic.mpy → modules/LM_haptic.mpy} +0 -0
  244. {micrOS/source → toolkit/workspace/precompiled/modules}/LM_i2c.py +5 -4
  245. toolkit/workspace/precompiled/modules/LM_i2s_mic.mpy +0 -0
  246. toolkit/workspace/precompiled/{LM_ld2410.mpy → modules/LM_ld2410.mpy} +0 -0
  247. toolkit/workspace/precompiled/modules/LM_light_sensor.mpy +0 -0
  248. toolkit/workspace/precompiled/modules/LM_mh_z19c.py +198 -0
  249. toolkit/workspace/precompiled/modules/LM_neoeffects.mpy +0 -0
  250. toolkit/workspace/precompiled/modules/LM_neopixel.mpy +0 -0
  251. toolkit/workspace/precompiled/{LM_oled.mpy → modules/LM_oled.mpy} +0 -0
  252. toolkit/workspace/precompiled/{LM_oled_sh1106.mpy → modules/LM_oled_sh1106.mpy} +0 -0
  253. toolkit/workspace/precompiled/modules/LM_oled_ui.mpy +0 -0
  254. toolkit/workspace/precompiled/modules/LM_pacman.mpy +0 -0
  255. toolkit/workspace/precompiled/modules/LM_presence.mpy +0 -0
  256. toolkit/workspace/precompiled/modules/LM_qmi8658.py +204 -0
  257. toolkit/workspace/precompiled/{LM_rencoder.py → modules/LM_rencoder.py} +2 -2
  258. toolkit/workspace/precompiled/modules/LM_rest.mpy +0 -0
  259. toolkit/workspace/precompiled/modules/LM_rgb.mpy +0 -0
  260. toolkit/workspace/precompiled/{LM_rgbcct.mpy → modules/LM_rgbcct.mpy} +0 -0
  261. toolkit/workspace/precompiled/modules/LM_roboarm.mpy +0 -0
  262. toolkit/workspace/precompiled/modules/LM_robustness.py +137 -0
  263. toolkit/workspace/precompiled/{LM_servo.mpy → modules/LM_servo.mpy} +0 -0
  264. toolkit/workspace/precompiled/{LM_sound_event.mpy → modules/LM_sound_event.mpy} +0 -0
  265. toolkit/workspace/precompiled/{LM_stepper.mpy → modules/LM_stepper.mpy} +0 -0
  266. toolkit/workspace/precompiled/modules/LM_switch.mpy +0 -0
  267. toolkit/workspace/precompiled/modules/LM_system.mpy +0 -0
  268. toolkit/workspace/precompiled/modules/LM_tcs3472.py +187 -0
  269. toolkit/workspace/precompiled/modules/LM_telegram.mpy +0 -0
  270. toolkit/workspace/precompiled/{LM_tinyrgb.mpy → modules/LM_tinyrgb.mpy} +0 -0
  271. toolkit/workspace/precompiled/{LM_trackball.mpy → modules/LM_trackball.mpy} +0 -0
  272. toolkit/workspace/precompiled/{LM_veml7700.mpy → modules/LM_veml7700.mpy} +0 -0
  273. toolkit/workspace/precompiled/modules/LM_web.mpy +0 -0
  274. toolkit/workspace/precompiled/urequests.mpy +0 -0
  275. {micrOS/source → toolkit/workspace/precompiled/web}/dashboard.html +4 -0
  276. toolkit/workspace/precompiled/web/editor.js +440 -0
  277. toolkit/workspace/precompiled/web/filesui.html +178 -0
  278. toolkit/workspace/precompiled/web/filesui.js +338 -0
  279. {micrOS/source → toolkit/workspace/precompiled/web}/index.html +44 -2
  280. toolkit/workspace/precompiled/{uapi.js → web/uapi.js} +48 -7
  281. toolkit/workspace/precompiled/{ustyle.css → web/ustyle.css} +6 -3
  282. micrOS/micropython/esp32-20241129-v1.24.1.bin +0 -0
  283. micrOS/micropython/esp32c3-20240222-v1.22.2.bin +0 -0
  284. micrOS/micropython/esp32s2-20240602-v1.23.0.bin +0 -0
  285. micrOS/micropython/esp32s2-LOLIN_MINI-20220618-v1.19.1.bin +0 -0
  286. micrOS/micropython/esp32s2-LOLIN_MINI-20240602-v1.23.0.bin +0 -0
  287. micrOS/micropython/esp32s3-20240105-v1.22.1.bin +0 -0
  288. micrOS/micropython/esp32s3_spiram_oct-20231005-v1.21.0.bin +0 -0
  289. micrOS/micropython/esp32s3_spiram_oct-20241129-v1.24.1.bin +0 -0
  290. micrOS/micropython/rpi-pico-w-20241129-v1.24.1.uf2 +0 -0
  291. micrOS/micropython/tinypico-20241129-v1.24.1.bin +0 -0
  292. micrOS/source/LM_L298N_DCmotor.py +0 -86
  293. micrOS/source/LM_catgame.py +0 -75
  294. micrOS/source/LM_dashboard_be.py +0 -37
  295. micrOS/source/LM_demo.py +0 -97
  296. micrOS/source/LM_espnow.py +0 -23
  297. micrOS/source/LM_intercon.py +0 -57
  298. micrOS/source/LM_keychain.py +0 -322
  299. micrOS/source/LM_lmpacman.py +0 -126
  300. micrOS/source/LM_neoeffects.py +0 -331
  301. micrOS/source/LM_oledui.py +0 -972
  302. micrOS/source/LM_pet_feeder.py +0 -78
  303. micrOS/source/LM_ph_sensor.py +0 -51
  304. micrOS/source/LM_robustness.py +0 -74
  305. micrOS/source/reset.py +0 -11
  306. micrOSDevToolKit-2.9.1.dist-info/RECORD +0 -365
  307. toolkit/dashboard_apps/AirQualityBME280.py +0 -36
  308. toolkit/dashboard_apps/AirQualityDHT22_CO2.py +0 -36
  309. toolkit/lib/file_extensions.py +0 -16
  310. toolkit/simulator_lib/__pycache__/sim_console.cpython-312.pyc +0 -0
  311. toolkit/simulator_lib/__pycache__/sim_console.cpython-38.pyc +0 -0
  312. toolkit/simulator_lib/__pycache__/sim_console.cpython-39.pyc +0 -0
  313. toolkit/workspace/precompiled/IO_esp32s3.mpy +0 -0
  314. toolkit/workspace/precompiled/IO_m5stamp.mpy +0 -0
  315. toolkit/workspace/precompiled/IO_qtpy.mpy +0 -0
  316. toolkit/workspace/precompiled/IO_rp2.mpy +0 -0
  317. toolkit/workspace/precompiled/IO_tinypico.mpy +0 -0
  318. toolkit/workspace/precompiled/LM_L298N_DCmotor.mpy +0 -0
  319. toolkit/workspace/precompiled/LM_OV2640.mpy +0 -0
  320. toolkit/workspace/precompiled/LM_catgame.py +0 -75
  321. toolkit/workspace/precompiled/LM_cct.mpy +0 -0
  322. toolkit/workspace/precompiled/LM_dashboard_be.py +0 -37
  323. toolkit/workspace/precompiled/LM_demo.py +0 -97
  324. toolkit/workspace/precompiled/LM_dimmer.mpy +0 -0
  325. toolkit/workspace/precompiled/LM_distance.mpy +0 -0
  326. toolkit/workspace/precompiled/LM_espnow.py +0 -23
  327. toolkit/workspace/precompiled/LM_genIO.mpy +0 -0
  328. toolkit/workspace/precompiled/LM_i2s_mic.mpy +0 -0
  329. toolkit/workspace/precompiled/LM_intercon.mpy +0 -0
  330. toolkit/workspace/precompiled/LM_keychain.mpy +0 -0
  331. toolkit/workspace/precompiled/LM_light_sensor.mpy +0 -0
  332. toolkit/workspace/precompiled/LM_lmpacman.mpy +0 -0
  333. toolkit/workspace/precompiled/LM_neoeffects.mpy +0 -0
  334. toolkit/workspace/precompiled/LM_neopixel.mpy +0 -0
  335. toolkit/workspace/precompiled/LM_oled_ui.mpy +0 -0
  336. toolkit/workspace/precompiled/LM_oledui.mpy +0 -0
  337. toolkit/workspace/precompiled/LM_pet_feeder.py +0 -78
  338. toolkit/workspace/precompiled/LM_ph_sensor.py +0 -51
  339. toolkit/workspace/precompiled/LM_presence.mpy +0 -0
  340. toolkit/workspace/precompiled/LM_rest.mpy +0 -0
  341. toolkit/workspace/precompiled/LM_rgb.mpy +0 -0
  342. toolkit/workspace/precompiled/LM_roboarm.mpy +0 -0
  343. toolkit/workspace/precompiled/LM_robustness.py +0 -74
  344. toolkit/workspace/precompiled/LM_switch.mpy +0 -0
  345. toolkit/workspace/precompiled/LM_system.mpy +0 -0
  346. toolkit/workspace/precompiled/LM_telegram.mpy +0 -0
  347. toolkit/workspace/precompiled/node_config.json +0 -1
  348. toolkit/workspace/precompiled/reset.mpy +0 -0
  349. /micrOS/source/{IO_esp32.py → modules/IO_esp32.py} +0 -0
  350. /micrOS/source/{IO_esp32c3.py → modules/IO_esp32c3.py} +0 -0
  351. /micrOS/source/{IO_esp32s2.py → modules/IO_esp32s2.py} +0 -0
  352. /micrOS/source/{IO_rp2.py → modules/IO_rp2.py} +0 -0
  353. /micrOS/source/{LM_gameOfLife.py → modules/LM_gameOfLife.py} +0 -0
  354. /micrOS/source/{LM_rgbcct.py → modules/LM_rgbcct.py} +0 -0
  355. /micrOS/source/{LM_rp2w.py → modules/LM_rp2w.py} +0 -0
  356. /micrOS/source/{LM_sdcard.py → modules/LM_sdcard.py} +0 -0
  357. /micrOS/source/{LM_sound_event.py → modules/LM_sound_event.py} +0 -0
  358. /micrOS/source/{LM_tinyrgb.py → modules/LM_tinyrgb.py} +0 -0
  359. /micrOS/source/{udashboard.js → web/udashboard.js} +0 -0
  360. /micrOS/source/{uwidgets.js → web/uwidgets.js} +0 -0
  361. /micrOS/source/{uwidgets_pro.js → web/uwidgets_pro.js} +0 -0
  362. {micrOSDevToolKit-2.9.1.dist-info → microsdevtoolkit-2.26.1.dist-info/licenses}/LICENSE +0 -0
  363. {micrOSDevToolKit-2.9.1.dist-info → microsdevtoolkit-2.26.1.dist-info}/top_level.txt +0 -0
  364. /toolkit/workspace/precompiled/{LM_rp2w.py → modules/LM_rp2w.py} +0 -0
  365. /toolkit/workspace/precompiled/{LM_sdcard.py → modules/LM_sdcard.py} +0 -0
  366. /toolkit/workspace/precompiled/{udashboard.js → web/udashboard.js} +0 -0
  367. /toolkit/workspace/precompiled/{uwidgets.js → web/uwidgets.js} +0 -0
  368. /toolkit/workspace/precompiled/{uwidgets_pro.js → web/uwidgets_pro.js} +0 -0
micrOS/source/Types.py CHANGED
@@ -11,8 +11,8 @@ USAGE:
11
11
  SLIDER brightness br=<0-100>
12
12
  SLIDER brightness br=<0-100-5>
13
13
  """
14
- from json import dumps
15
- from Debug import errlog_add
14
+ from json import dumps, loads
15
+ from Debug import syslog
16
16
 
17
17
  ########################################################
18
18
  # HELP TUPLE RESOLVER #
@@ -80,19 +80,63 @@ def _generate(type_dict, help_msg):
80
80
  return dumps(type_dict | overwrite)
81
81
 
82
82
 
83
+ def _extract_tag_and_overrides(msg):
84
+ """
85
+ Example:
86
+ "TEXTBOX{'refresh': 5000} measure ntfy=False"
87
+ OR with default refresh value:
88
+ "TEXTBOX measure ntfy=False"
89
+ return:
90
+ tag -> "TEXTBOX"
91
+ overrides(optional) -> {'refresh': 5000}
92
+ cmd -> "measure ntfy=False"
93
+ """
94
+ msg = msg.strip()
95
+ if not msg:
96
+ return "", {}, ""
97
+ tag_end = len(msg)
98
+ for i, c in enumerate(msg):
99
+ if c in " {":
100
+ tag_end = i
101
+ break
102
+ tag = msg[:tag_end]
103
+ cmd = msg[tag_end:].lstrip()
104
+ overrides = {}
105
+ if tag.isupper() and cmd.startswith('{'):
106
+ i = cmd.find('}')
107
+ if i >= 0:
108
+ try:
109
+ overrides = loads('{' + cmd[1:i].replace("'", '"') + '}')
110
+ except Exception as e:
111
+ syslog(f"[ERR] Types tag overrides: {e}")
112
+ cmd = cmd[i + 1:].lstrip()
113
+ return tag, overrides, cmd
114
+
115
+
83
116
  def resolve(help_data, widgets=False):
84
117
  help_msg = []
85
118
  for msg in help_data:
86
- tag = msg.split()[0].strip()
87
- if tag.isupper():
88
- resolved_tag = globals().get(tag, tag)()
89
- if widgets and isinstance(resolved_tag, dict):
119
+ tag, overrides, cmd = _extract_tag_and_overrides(msg)
120
+ if tag and tag.isupper():
121
+ # TAG exists in help message
122
+ if widgets:
123
+ # Format output as widget - machine-readable
124
+ factory = globals().get(tag, None)
125
+ resolved_tag = factory() if callable(factory) else factory
126
+ if isinstance(resolved_tag, dict) and overrides:
127
+ # Apply inline widget-only overrides, e.g. TEXTBOX{'refresh': 5000}
128
+ resolved_tag.update(overrides)
90
129
  try:
91
- help_msg.append(_generate(resolved_tag, msg))
130
+ # Build a clean message for _generate, without inline {...}
131
+ cleaned_msg = (tag + ' ' + cmd).strip()
132
+ # Generate JSON output with TAG
133
+ help_msg.append(_generate(resolved_tag, cleaned_msg))
92
134
  except Exception as e:
93
- errlog_add(f"[ERR] resolve {tag} help msg: {e}")
135
+ syslog(f"[ERR] resolve {tag} help msg: {e}")
94
136
  continue
95
- help_msg.append(msg.replace(tag, '').strip())
137
+ # Widgets OFF - TAG exists - remove TAG from output
138
+ help_msg.append(cmd.strip())
96
139
  elif not widgets:
140
+ # No TAG - Widgets OFF output
97
141
  help_msg.append(msg)
98
142
  return tuple(help_msg)
micrOS/source/Web.py CHANGED
@@ -5,160 +5,358 @@ Built-in-function:
5
5
  - response
6
6
  - landing page: index.html
7
7
  - rest/ - call load modules, e.x.: system/top
8
- - file response (.html, .css, .js, .jped) - generic file server feature
8
+ - file response (.html, .css, .js, .jpeg) - generic file server feature
9
9
  - "virtual" endpoints - to reply from script on a defined endpoint
10
10
  - stream - stream data (jpeg) function
11
11
 
12
- Designed by Marcell Ban aka BxNxM
12
+ Designed by Marcell Ban aka BxNxM and szeka9 (GitHub)
13
13
  """
14
14
 
15
+ from re import compile
15
16
  from json import dumps, loads
17
+ from uos import stat
16
18
  import uasyncio as asyncio
17
19
  from Tasks import lm_exec, NativeTask, lm_is_loaded
18
- from Debug import errlog_add, console_write
20
+ from Debug import syslog, console_write
19
21
  from Config import cfgget
22
+ from Files import OSPath, path_join, abs_path
23
+ try:
24
+ from gc import mem_free, collect
25
+ except:
26
+ from simgc import mem_free, collect # simulator mode
27
+
28
+
29
+ class ServerBusyException(Exception):
30
+ pass
31
+
32
+ class ConnectionError(Exception):
33
+ pass
34
+
35
+ class HeaderDecodingError(Exception):
36
+ pass
37
+
38
+ def url_path_resolve(path:str) -> tuple[bool, str]:
39
+ """
40
+ :param path: input path
41
+ Return: isError, absolutePath
42
+ """
43
+ # $Extended mount check: WEB_MOUNTS (/modules and /web)
44
+ path = path.lstrip("/")
45
+ if path.startswith("$"):
46
+ mount_alias = path.split("/")[0]
47
+ mount_path = WebEngine.WEB_MOUNTS.get(mount_alias, None)
48
+ if mount_path is None:
49
+ return True, f"Invalid mount point: {mount_alias}"
50
+ mount_path = path.replace(mount_alias, mount_path)
51
+ return False, mount_path
52
+ # Default web path: /web
53
+ return False, path_join(OSPath.WEB, path)
20
54
 
21
55
 
22
56
  class WebEngine:
23
57
  __slots__ = ["client"]
24
- REST_ENDPOINTS = {}
58
+ ENDPOINTS = {}
25
59
  AUTH = cfgget('auth')
26
60
  VERSION = "n/a"
27
61
  REQ200 = "HTTP/1.1 200 OK\r\nContent-Type: {dtype}\r\nContent-Length:{len}\r\n\r\n{data}"
62
+ REQ200_CHUNKED = "HTTP/1.1 200 OK\r\nContent-Type: {dtype}\r\nTransfer-Encoding: chunked\r\n\r\n"
28
63
  REQ400 = "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nContent-Length: {len}\r\n\r\n{data}"
29
64
  REQ404 = "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: {len}\r\n\r\n{data}"
65
+ REQ500 = "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\nContent-Length: {len}\r\n\r\n{data}"
66
+ REQ503 = "HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/plain\r\nContent-Length: {len}\r\n\r\n{data}"
67
+ CONTENT_TYPES = {"html": "text/html",
68
+ "css": "text/css",
69
+ "js": "application/javascript",
70
+ "json": "application/json",
71
+ "ico": "image/x-icon", # favicon
72
+ "jpeg": "image/jpeg",
73
+ "png": "image/png",
74
+ "gif": "image/gif"}
75
+ METHODS = ("GET", "POST", "DELETE")
76
+ WEB_MOUNTS = {}
77
+ # MEMORY DIMENSIONING FOR THE BEST PERFORMANCE
78
+ # (is_limited, free_mem, min_mem_req_kb, chunk_threshold_kb, chunk_size_bytes)
79
+ MEM_DIM = (None, -1, 20, 2, 1024)
80
+ READ_TIMEOUT_SEC = 10
30
81
 
31
82
  def __init__(self, client, version):
32
83
  self.client = client
33
84
  WebEngine.VERSION = version
34
85
 
86
+ async def a_send(self, response:str, encode:str='utf8'):
87
+ raise NotImplementedError("Child class must implement a_send coroutine.")
88
+
35
89
  @staticmethod
36
- def file_type(path):
90
+ def file_type(path:str):
37
91
  """File dynamic Content-Type handling"""
38
- content_types = {".html": "text/html",
39
- ".css": "text/css",
40
- ".js": "application/javascript"}
92
+ default_type = "text/plain"
41
93
  # Extract the file extension
42
94
  ext = path.rsplit('.', 1)[-1]
43
95
  # Return the content type based on the file extension
44
- return content_types.get(f".{ext}", "text/plain")
45
-
46
- async def response(self, request):
47
- """HTTP GET REQUEST - WEB INTERFACE"""
48
- # Parse request line (first line)
49
- _method, url, _version = request.split('\n')[0].split()
50
- # Protocol validation
51
- if _method != "GET" and _version.startswith('HTTP'):
52
- _err = "Bad Request: not GET HTTP/1.1"
53
- await self.client.a_send(self.REQ400.format(len=len(_err), data=_err))
54
- return
96
+ return WebEngine.CONTENT_TYPES.get(ext, default_type)
55
97
 
98
+ @staticmethod
99
+ def parse_headers(raw_headers:bytes):
100
+ """Basic parser to extract HTTP/MIME headers without guarantees on RFC compliance"""
101
+ header_lines = raw_headers.decode('ascii').split('\r\n')
102
+ headers = {}
103
+ for line in header_lines:
104
+ # TODO: support for UTF-8 in field values (e.g filenames), can be board dependent
105
+ if any(ord(c) > 127 for c in line):
106
+ raise HeaderDecodingError('Non-ASCII character found in the request')
107
+ if ':' not in line:
108
+ continue
109
+ name, value = line.split(':', 1)
110
+ headers[name.strip().lower()] = value.strip()
111
+ return headers
112
+
113
+ @staticmethod
114
+ def dimensioning():
115
+ # (is_limited, free_mem, min_mem_req_kb, chunk_threshold_kb, chunk_size_bytes)
116
+ if WebEngine.MEM_DIM[0] is None:
117
+ collect()
118
+ mfree = mem_free() // 1024 # <- bytes->kb
119
+ if mfree < WebEngine.MEM_DIM[2]:
120
+ # Too low memory - No Web UI - under 20kb
121
+ WebEngine.MEM_DIM = (True, mfree) + WebEngine.MEM_DIM[2:]
122
+ return WebEngine.MEM_DIM
123
+ if mfree < WebEngine.MEM_DIM[2] * 5:
124
+ # Normal: default memory setup - Web UI - under 100kb
125
+ WebEngine.MEM_DIM = (False, mfree) + WebEngine.MEM_DIM[2:]
126
+ return WebEngine.MEM_DIM
127
+ # Large memory - Web UI - over 100kb
128
+ upscale = max(1, min(25, int((mfree // WebEngine.MEM_DIM[2]) // 2))) # ~50% free mem budget
129
+ WebEngine.MEM_DIM = (False, mfree, WebEngine.MEM_DIM[2],
130
+ WebEngine.MEM_DIM[3]*upscale, WebEngine.MEM_DIM[4]*upscale)
131
+ syslog(f"[INFO] WebEngine ChunkUpscale ({upscale}x): {WebEngine.MEM_DIM}")
132
+ return WebEngine.MEM_DIM
133
+
134
+ @staticmethod
135
+ def register(endpoint:str, callback:object|str, method:str='GET') -> None:
136
+ """
137
+ PUBLIC METHOD FOR LMs: Webengine endpoint registration handler
138
+ :param endpoint: name of the endpoint
139
+ :param callback: callback function (WebEngine compatible: return: html_type, content)
140
+ :param method: HTTP method name
141
+ """
142
+ if not endpoint in WebEngine.ENDPOINTS:
143
+ WebEngine.ENDPOINTS[endpoint] = {}
144
+ if method not in WebEngine.METHODS:
145
+ raise ValueError(f"method must be one of {WebEngine.METHODS}")
146
+ WebEngine.ENDPOINTS[endpoint][method] = callback
147
+ return
148
+
149
+ @staticmethod
150
+ def web_mounts(modules:bool=None, data:bool=None, logs:bool=None) -> dict:
151
+ """
152
+ PUBLIC METHOD FOR LMs: WebEngine access path handler
153
+ - default path: /web
154
+ - extended path access: with $modules and $data dirs
155
+ """
156
+ def _update(state, alias, path):
157
+ if state:
158
+ WebEngine.WEB_MOUNTS[alias] = path
159
+ elif WebEngine.WEB_MOUNTS.get(alias, False):
160
+ del WebEngine.WEB_MOUNTS[alias]
161
+ if modules is not None:
162
+ # Set modules dir access
163
+ _update(modules, "$modules", OSPath.MODULES)
164
+ if data is not None:
165
+ # Set data dir access
166
+ _update(data, "$data", OSPath.DATA)
167
+ if logs is not None:
168
+ # Set logs dir access
169
+ _update(logs, "$logs", OSPath.LOGS)
170
+ return WebEngine.WEB_MOUNTS
171
+
172
+ async def response(self, request:bytes) -> bool:
173
+ """HTTP GET/POST REQUEST - WEB INTERFACE"""
174
+ # [0] PROTOCOL VALIDATION AND PARSING
175
+ if not request:
176
+ _err = "Empty request"
177
+ await self.a_send(self.REQ400.format(len=len(_err), data=_err))
178
+ return True
179
+ status_line = request.split(b'\r\n', 1)[0]
180
+ status_parts = status_line.decode('ascii').split()
181
+ if len(status_parts) != 3:
182
+ if status_parts[0] not in self.METHODS:
183
+ # INVALID REQUEST - REQUEST OVERFLOW - NO RESPONSE
184
+ syslog(f"[WARN] WebCli REQ Overflow: {len(status_parts)}")
185
+ return False # Close connection...
186
+ _err = "Malformed request line"
187
+ await self.a_send(self.REQ400.format(len=len(_err), data=_err))
188
+ return True
189
+ _method, url, _version = status_parts
190
+ url = abs_path(url)
191
+ if _method not in self.METHODS or not _version.startswith('HTTP/'):
192
+ _err = f"Unsupported method: {_method} {_version}"
193
+ await self.a_send(self.REQ400.format(len=len(_err), data=_err))
194
+ return True
195
+ payload = request[len(status_line):]
196
+ blank_idx = payload.find(b'\r\n\r\n')
197
+ try:
198
+ if blank_idx > -1:
199
+ headers = self.parse_headers(payload[0:blank_idx])
200
+ body = payload[blank_idx + 4:]
201
+ else:
202
+ headers = self.parse_headers(payload)
203
+ body = b''
204
+ except HeaderDecodingError:
205
+ await self.client.a_send(self.REQ400.format(len=18, data='400 Invalid Header'))
206
+ return True
56
207
  # [1] REST API GET ENDPOINT [/rest]
57
- if url.startswith('/rest'):
208
+ if url.startswith('/rest') and _method == "GET":
58
209
  self.client.console("[WebCli] --- /rest ACCEPT")
59
210
  try:
60
211
  await self.client.a_send(WebEngine.rest(url))
61
212
  except Exception as e:
62
213
  await self.client.a_send(self.REQ404.format(len=len(str(e)), data=e))
63
- return
214
+ return True
64
215
  # [2] DYNAMIC/USER ENDPOINTS (from Load Modules)
65
- if await self.endpoints(url):
66
- return
216
+ if await self.endpoints(url, _method, headers, body):
217
+ return True
218
+ # MEMORY DIMENSIONING
219
+ mem_limited, free, *_ = self.dimensioning()
220
+ if mem_limited:
221
+ _err = f"Low memory ({free} kb): serving API only."
222
+ await self.a_send(self.REQ400.format(len=len(_err), data=_err))
223
+ return True
67
224
  # [3] HOME/PAGE ENDPOINT(s) [default: / -> /index.html]
68
- if url.startswith('/'):
69
- resource = 'index.html' if url == '/' else url.replace('/', '')
70
- self.client.console(f"[WebCli] --- {url} ACCEPT")
71
- if resource.split('.')[-1] not in ('html', 'js', 'css'):
72
- await self.client.a_send(self.REQ404.format(len=27, data='404 Not supported file type'))
73
- return
225
+ if url.startswith('/') and _method == "GET":
226
+ resource = 'index.html' if url == '/' else url.lstrip('/')
227
+ self.client.console(f"[WebCli] --- {url} ACCEPT -> {resource}")
228
+ if "/" not in resource and resource.split('.')[-1] not in self.CONTENT_TYPES:
229
+ # Validate /web root types only - otherwise default fallback type for unknowns: "text/plain"
230
+ await self.client.a_send(self.REQ404.format(len=27, data='404 Not Supported File Type'))
231
+ return True
74
232
  try:
75
- # SEND RESOURCE CONTENT: HTML, JS, CSS
76
- with open(resource, 'r') as file:
77
- data = file.read()
78
- response = self.REQ200.format(dtype=WebEngine.file_type(resource), len=len(data), data=data)
79
- # Send entire response data
80
- await self.client.a_send(response)
81
-
82
- # Send chunks of response data (experimental)
83
- #response_len, chunk_size, position = len(response), 300, 0
84
- #while position < response_len:
85
- # # Calculate the size of the next chunk
86
- # next_chunk_size = min(chunk_size, response_len - position)
87
- # chunk = response[position:position + next_chunk_size]
88
- # await self.client.a_send(chunk)
89
- # position += next_chunk_size
90
- except Exception as e:
91
- if 'memory allocation failed' in str(e):
92
- errlog_add(f"[ERR] WebCli {resource}: {e}")
233
+ # SEND RESOURCE CONTENT: HTML, JS, CSS (WebEngine.CONTENT_TYPES)
234
+ await self.file_transfer(resource)
235
+ except OSError:
93
236
  await self.client.a_send(self.REQ404.format(len=13, data='404 Not Found'))
94
- return
237
+ except MemoryError as e:
238
+ syslog(f"[ERR] WebCli {resource}: {e}")
239
+ await self.client.a_send(self.REQ500.format(len=17, data='500 Out of Memory'))
240
+ except Exception as e:
241
+ syslog(f"[ERR] WebCli {resource}: {e}")
242
+ await self.client.a_send(self.REQ500.format(len=16, data='500 Server Error'))
243
+ return True
95
244
  # INVALID/BAD REQUEST
96
245
  await self.client.a_send(self.REQ400.format(len=15, data='400 Bad Request'))
246
+ return True
97
247
 
98
248
  @staticmethod
99
249
  def rest(url):
100
- resp_schema = {'result': None, 'state': False}
250
+ resp_schema = {'result': {}, 'state': False}
101
251
  cmd = url.replace('/rest', '')
102
252
  if len(cmd) > 1:
103
- # REST sub-parameter handling (rest commands)
104
- cmd = (cmd.replace('/', ' ').replace('%22', '"').replace('%E2%80%9C', '"')
105
- .replace('%E2%80%9D', '"').replace('-', ' ').strip().split())
253
+ # REST sub-parameter handling (rest commands) TODO: create url_decode helper for: " ' >
254
+ cmd = (cmd.replace('/', ' ').replace('-', ' ').replace("%3E", ">")
255
+ .replace('%22', '"').replace('%E2%80%9C', '"').replace('%E2%80%9D', '"')
256
+ .strip().split())
106
257
  # EXECUTE COMMAND - LoadModule
107
258
  if WebEngine.AUTH:
108
259
  state, out = lm_exec(cmd, jsonify=True) if lm_is_loaded(cmd[0]) else (True, 'Auth:Protected')
109
260
  else:
110
261
  state, out = lm_exec(cmd, jsonify=True)
111
262
  try:
112
- resp_schema['result'] = loads(out) # Load again ... hack for embedded shell json converter...
263
+ resp_schema['result'] = loads(out) # Load again ... hack for embedded json converter...
113
264
  except:
114
265
  resp_schema['result'] = out
115
266
  resp_schema['state'] = state
116
267
  else:
117
268
  resp_schema['result'] = {"micrOS": WebEngine.VERSION, 'node': cfgget('devfid'), 'auth': WebEngine.AUTH}
118
- if len(tuple(WebEngine.REST_ENDPOINTS.keys())) > 0:
119
- resp_schema['result']['usr_endpoints'] = tuple(WebEngine.REST_ENDPOINTS)
269
+ if len(tuple(WebEngine.ENDPOINTS.keys())) > 0:
270
+ resp_schema['result']['usr_endpoints'] = tuple(WebEngine.ENDPOINTS)
120
271
  resp_schema['state'] = True
121
272
  response = dumps(resp_schema)
122
273
  return WebEngine.REQ200.format(dtype='text/html', len=len(response), data=response)
123
274
 
124
- async def endpoints(self, url):
275
+ async def endpoints(self, url:str, method:str, headers:dict, body:bytes):
125
276
  url = url[1:] # Cut first / char
126
- if url in WebEngine.REST_ENDPOINTS:
277
+ if url in WebEngine.ENDPOINTS and method in WebEngine.ENDPOINTS[url]: # TODO: support for query parameters
127
278
  console_write(f"[WebCli] endpoint: {url}")
128
279
  # Registered endpoint was found - exec callback
129
280
  try:
130
281
  # RESOLVE ENDPOINT CALLBACK
131
282
  # dtype:
132
- # one-shot: image/jpeg | text/html | text/plain - data: raw
133
- # task: multipart/x-mixed-replace | multipart/form-data - data: dict=callback,content-type
134
- # content-type: image/jpeg | audio/l16;*
135
- dtype, data = WebEngine.REST_ENDPOINTS[url]()
283
+ # one-shot (simple MIME types): image/jpeg | text/html | text/plain - data: raw
284
+ # task (streaming MIME types): multipart/x-mixed-replace | multipart/form-data - data: dict{callback,content-type}
285
+ # content-type: image/jpeg | audio/l16;*
286
+ if body and (response := await self.handle_with_body(url, method, headers, body)):
287
+ dtype, data = response
288
+ else:
289
+ # TODO: contract needed for passing headers
290
+ callback = WebEngine.ENDPOINTS[url][method]
291
+ if callable(callback):
292
+ dtype, data = WebEngine.ENDPOINTS[url][method]()
293
+ else:
294
+ # Endpoint callback is a file reference
295
+ await self.file_transfer(callback)
296
+ return True
297
+
136
298
  if dtype == 'image/jpeg':
137
- resp = f"HTTP/1.1 200 OK\r\nContent-Type: {dtype}\r\nContent-Length:{len(data)}\r\n\r\n".encode(
138
- 'utf8') + data
299
+ resp = f"HTTP/1.1 200 OK\r\nContent-Type: {dtype}\r\nContent-Length:{len(data)}\r\n\r\n".encode('ascii') + data
139
300
  await self.client.a_send(resp, encode=None)
140
301
  elif dtype in ('multipart/x-mixed-replace', 'multipart/form-data'):
141
- headers = (
142
- f"HTTP/1.1 200 OK\r\nContent-Type: {dtype}; boundary=\"micrOS_boundary\"\r\n\r\n").encode(
143
- 'utf-8')
144
- await self.client.a_send(headers, encode=None)
302
+ resp_headers = f"HTTP/1.1 200 OK\r\nContent-Type: {dtype}; boundary=\"micrOS_boundary\"\r\n\r\n"
303
+ await self.client.a_send(resp_headers)
145
304
  # Start Native stream async task
146
305
  task = NativeTask()
147
- task.create(callback=self.stream(data['callback'], task, data['content-type']),
148
- tag=f"web.stream_{self.client.client_id.replace('W', '')}")
306
+ tag=f"web.stream_{self.client.client_id.replace('W', '')}"
307
+ task.create(callback=self.stream(data['callback'], task, data['content-type']), tag=tag)
149
308
  else: # dtype: text/html or text/plain
150
- await self.client.a_send(
151
- f"HTTP/1.1 200 OK\r\nContent-Type: {dtype}\r\nContent-Length:{len(data)}\r\n\r\n{data}")
309
+ await self.client.a_send(WebEngine.REQ200.format(dtype=dtype, len=len(data), data=data))
310
+ except ServerBusyException as e:
311
+ await self.client.a_send(self.REQ503.format(len=len(str(e)), data=e))
312
+ except HeaderDecodingError as e:
313
+ await self.client.a_send(self.REQ400.format(len=len(str(e)), data=e))
152
314
  except Exception as e:
153
- await self.client.a_send(self.REQ404.format(len=len(str(e)), data=e))
154
- errlog_add(f"[ERR] WebCli endpoints {url}: {e}")
315
+ if self.client.connected:
316
+ await self.client.a_send(self.REQ400.format(len=len(str(e)), data=e))
317
+ err = f"WebCli endpoints {url}: {e}" if "ReadOnly" in str(e) else f"[ERR] WebCli endpoints {url}: {e}"
318
+ syslog(err)
155
319
  return True # Registered endpoint was found and executed
156
320
  return False # Not registered endpoint
157
321
 
322
+ async def file_transfer(self, web_resource:str):
323
+ """
324
+ Send a file to the client using either normal or chunked HTTP transfer.
325
+ :param web_resource: Path to the file to be served.
326
+ """
327
+ # Resolve
328
+ err, web_resource = url_path_resolve(web_resource)
329
+ if err:
330
+ await self.client.a_send(self.REQ404.format(len=19, data='404 Mount Not Found'))
331
+ return False
332
+ with open(web_resource, "rb") as file:
333
+ chunking_threshold_kb = WebEngine.MEM_DIM[3]
334
+ chunk_size_bytes = WebEngine.MEM_DIM[4]
335
+ if stat(web_resource)[6] > chunking_threshold_kb * 1024:
336
+ # Chunked HTTP transfer
337
+ response = self.REQ200_CHUNKED.format(dtype=WebEngine.file_type(web_resource))
338
+ await self.client.a_send(response)
339
+ data = file.read(chunk_size_bytes)
340
+ while data:
341
+ await self.client.a_send(f"{len(data):X}\r\n".encode(), None)
342
+ await self.client.a_send(data, None)
343
+ await self.client.a_send(b'\r\n', None)
344
+ data = file.read(chunk_size_bytes)
345
+ await self.client.a_send(b'0\r\n\r\n', None)
346
+ return
347
+ # Normal HTTP transfer
348
+ data = file.read()
349
+ response = self.REQ200.format(dtype=WebEngine.file_type(web_resource), len=len(data), data="")
350
+ await self.client.a_send(response)
351
+ await self.client.a_send(data, None)
352
+ return True
353
+
158
354
  async def stream(self, callback, task, content_type):
159
355
  """
160
- Async stream method
356
+ Async stream method.
161
357
  :param callback: sync or async function callback (auto-detect) WARNING: works for functions only (not methods!)
358
+ :param task: NativeTask instance (that the stream runs in)
359
+ :param content_type: image/jpeg | audio/l16;*
162
360
  """
163
361
  is_coroutine = 'generator' in str(type(callback)) # async function callback auto-detect
164
362
  with task:
@@ -167,7 +365,7 @@ class WebEngine:
167
365
 
168
366
  while self.client.connected and data_to_send is not None:
169
367
  data_to_send = await callback() if is_coroutine else callback()
170
- part = (f"\r\n--micrOS_boundary\r\nContent-Type: {content_type}\r\n\r\n").encode('utf-8') + data_to_send
368
+ part = (f"\r\n--micrOS_boundary\r\nContent-Type: {content_type}\r\n\r\n").encode('ascii') + data_to_send
171
369
  task.out = 'Data sent'
172
370
  await self.client.a_send(part, encode=None)
173
371
  await asyncio.sleep_ms(10)
@@ -175,6 +373,113 @@ class WebEngine:
175
373
  # Gracefully terminate the stream
176
374
  if self.client.connected:
177
375
  closing_boundary = '\r\n--micrOS_boundary--\r\n'
178
- await self.client.a_send(closing_boundary, encode=None)
376
+ await self.client.a_send(closing_boundary)
179
377
  await self.client.close()
180
378
  task.out = 'Finished stream'
379
+
380
+ async def handle_with_body(self, url:str, method:str, headers:dict, body:bytes):
381
+ """
382
+ Handle requests with a body.
383
+ :param url: synchronous function callback, expecting parts passed as bytes
384
+ :param boundary: boundary parameter
385
+ :param body: request body, handled depending on headers (e.g. content type)
386
+ """
387
+ content_length = int(headers.get("content-length", -1))
388
+ is_multipart = False
389
+ # [1] Header parsing
390
+ if headers:
391
+ if 'content-type' in headers:
392
+ multipart_regex = compile("multipart/form-data;\s*boundary=\"?([^\";\r\n]+)\"?")
393
+ if (multipart_match := multipart_regex.match(headers['content-type'])):
394
+ is_multipart = True
395
+ multipart_boundary = multipart_match.group(1).encode('ascii')
396
+ # [2] Header dependent body handling
397
+ if is_multipart:
398
+ return await self.recv_multipart(WebEngine.ENDPOINTS[url][method], multipart_boundary, bytearray(body), content_length)
399
+ # [3] Default handling
400
+ # TODO: contract needed for passing headers
401
+ return WebEngine.ENDPOINTS[url][method](body)
402
+
403
+ async def recv_multipart(self, callback, boundary:bytes, part_buffer:bytearray, content_length:int):
404
+ """
405
+ Receives multipart body and runs a callback on each complete part.
406
+ :param callback: synchronous callback function clb(part_headers, part_body), parsed headers and raw body
407
+ :param boundary: boundary parameter
408
+ :param part_buffer: contains initial request body, remaining parts are read if the body contains no closing delimiter
409
+ :param content_length: content length number
410
+ """
411
+ dtype = 'text/plain'
412
+ data = 'failed to parse'
413
+ delimiter = b'--' + boundary
414
+ delimiter_line = delimiter + b'\r\n'
415
+ close_delimiter = delimiter + b'--'
416
+ at_start = True
417
+ first_part = True
418
+ actual_length = len(part_buffer)
419
+
420
+ if content_length <= 0:
421
+ raise ValueError("Invalid content-length")
422
+
423
+ async def read_more():
424
+ error, more = await self.client.read(decoding=None, timeout_seconds=self.READ_TIMEOUT_SEC)
425
+ if error:
426
+ await self.client.close()
427
+ raise ConnectionError()
428
+ if not more:
429
+ raise ValueError('Unexpected EOF in multipart body')
430
+ return more
431
+
432
+ def parse_part(part:bytes):
433
+ blank_idx = part.find(b'\r\n\r\n')
434
+ if blank_idx == -1:
435
+ raise ValueError('Headers could not be parsed')
436
+ headers = self.parse_headers(part[:blank_idx])
437
+ body = part[blank_idx + 4:]
438
+ return headers, body
439
+
440
+ while True:
441
+ # [1] Read until a complete part is received
442
+ while b'\r\n' not in part_buffer:
443
+ more = await read_more()
444
+ if actual_length + len(more) > content_length:
445
+ raise ValueError('Invalid content-length')
446
+ part_buffer += more
447
+ actual_length += len(more)
448
+ if at_start:
449
+ if not part_buffer.startswith(delimiter_line):
450
+ raise ValueError('Missing initial multipart boundary')
451
+ part_buffer = part_buffer[len(delimiter_line):]
452
+ at_start = False
453
+ continue
454
+ idx = part_buffer.find(b'\r\n' + delimiter)
455
+ if idx == -1:
456
+ more = await read_more()
457
+ if actual_length + len(more) > content_length:
458
+ raise ValueError('Invalid content-length')
459
+ part_buffer += more
460
+ actual_length += len(more)
461
+ continue
462
+ # [2] Complete part received
463
+ part = part_buffer[:idx]
464
+ part_buffer = part_buffer[idx + 2:] # Consume leading CRLF
465
+ if part_buffer.startswith(close_delimiter):
466
+ # Last part found, stop processing delimiters
467
+ if part:
468
+ part_headers, part_body = parse_part(part)
469
+ dtype, data = callback(part_headers, part_body, first=first_part, last=True)
470
+ break
471
+ if not part_buffer.startswith(delimiter_line):
472
+ raise ValueError('Invalid multipart boundary formatting')
473
+ part_buffer = part_buffer[len(delimiter_line):]
474
+ if part:
475
+ # Process complete part
476
+ part_headers, part_body = parse_part(part)
477
+ dtype, data = callback(part_headers, part_body, first=first_part)
478
+ first_part = False
479
+ # [3] Verify content length
480
+ if actual_length < content_length:
481
+ more = await read_more()
482
+ if actual_length + len(more) != content_length:
483
+ raise ValueError('Invalid content-length')
484
+ # Ignore remaining content
485
+ return dtype, data
File without changes