micrOSDevToolKit 2.1.5__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.
- env/driver_cp210x/.DS_Store +0 -0
- env/driver_cp210x/macOS_VCP_Driver/SiLabsUSBDriverDisk.dmg +0 -0
- env/driver_cp210x/macOS_VCP_Driver/macOS_VCP_Driver_Release_Notes.txt +17 -1
- micrOS/micropython/esp32-20251209-v1.27.0.bin +0 -0
- micrOS/micropython/esp32c3-20251209-v1.27.0.bin +0 -0
- micrOS/micropython/esp32c6-20251209-v1.27.0.bin +0 -0
- micrOS/micropython/esp32s2-20251209-v1.27.0.bin +0 -0
- micrOS/micropython/esp32s2-LOLIN_MINI-20251209-v1.27.0.bin +0 -0
- micrOS/micropython/esp32s3-4MBflash-20241129-v1.24.1.bin +0 -0
- micrOS/micropython/esp32s3-8MBflash-20251209-v1.27.0.bin +0 -0
- micrOS/micropython/esp32s3_spiram_oct-20251209-v1.27.0.bin +0 -0
- micrOS/micropython/rpi-pico-w-20251209-v1.27.0.uf2 +0 -0
- micrOS/micropython/tinypico-20251209-v1.27.0.bin +0 -0
- micrOS/release_info/micrOS_ReleaseInfo/system_analysis_sum.json +191 -151
- micrOS/source/Auth.py +37 -0
- micrOS/source/Common.py +376 -102
- micrOS/source/Config.py +55 -25
- micrOS/source/Debug.py +54 -193
- micrOS/source/Espnow.py +404 -0
- micrOS/source/Files.py +207 -0
- micrOS/source/Hooks.py +88 -16
- micrOS/source/InterConnect.py +130 -46
- micrOS/source/Interrupts.py +8 -8
- micrOS/source/Logger.py +131 -0
- micrOS/source/Network.py +41 -21
- micrOS/source/Notify.py +74 -198
- micrOS/source/Pacman.py +326 -0
- micrOS/source/Scheduler.py +18 -55
- micrOS/source/Server.py +84 -217
- micrOS/source/Shell.py +103 -93
- micrOS/source/Tasks.py +239 -173
- micrOS/source/Time.py +21 -22
- micrOS/source/Types.py +89 -54
- micrOS/source/Web.py +485 -0
- micrOS/source/__pycache__/Common.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/Debug.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/Files.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/Logger.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/Scheduler.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/Server.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/Shell.cpython-312.pyc +0 -0
- micrOS/source/__pycache__/replhelper.cpython-312.pyc +0 -0
- micrOS/source/helpers.py +132 -0
- micrOS/source/micrOS.py +25 -21
- micrOS/source/micrOSloader.py +14 -23
- micrOS/source/microIO.py +94 -57
- toolkit/simulator_lib/LP_darwin.py → micrOS/source/modules/IO_esp32.py +22 -11
- micrOS/source/{IO_esp32c3.py → modules/IO_esp32c3.py} +6 -1
- micrOS/source/modules/IO_esp32c6.py +38 -0
- micrOS/source/{IO_esp32s2.py → modules/IO_esp32s2.py} +6 -1
- micrOS/source/{IO_esp32s3.py → modules/IO_esp32s3.py} +43 -2
- micrOS/source/modules/IO_m5stamp.py +86 -0
- micrOS/source/{IO_qtpy.py → modules/IO_qtpy.py} +28 -18
- micrOS/source/{IO_tinypico.py → modules/IO_tinypico.py} +48 -3
- micrOS/source/modules/LM_L298N.py +161 -0
- {toolkit/workspace/precompiled → micrOS/source/modules}/LM_L9110_DCmotor.py +4 -4
- micrOS/source/{LM_OV2640.py → modules/LM_OV2640.py} +53 -42
- micrOS/source/{LM_VL53L0X.py → modules/LM_VL53L0X.py} +5 -5
- micrOS/source/{LM_aht10.py → modules/LM_aht10.py} +12 -4
- micrOS/source/{LM_bme280.py → modules/LM_bme280.py} +13 -25
- micrOS/source/{LM_buzzer.py → modules/LM_buzzer.py} +42 -40
- micrOS/source/{LM_cct.py → modules/LM_cct.py} +22 -27
- micrOS/source/modules/LM_cluster.py +255 -0
- micrOS/source/{LM_co2.py → modules/LM_co2.py} +13 -6
- micrOS/source/{LM_dht11.py → modules/LM_dht11.py} +13 -29
- micrOS/source/{LM_dht22.py → modules/LM_dht22.py} +13 -28
- micrOS/source/{LM_dimmer.py → modules/LM_dimmer.py} +19 -16
- micrOS/source/modules/LM_distance.py +135 -0
- micrOS/source/{LM_ds18.py → modules/LM_ds18.py} +12 -4
- micrOS/source/{LM_esp32.py → modules/LM_esp32.py} +16 -4
- micrOS/source/modules/LM_espnow.py +53 -0
- micrOS/source/modules/LM_fileserver.py +265 -0
- micrOS/source/{LM_gameOfLife.py → modules/LM_gameOfLife.py} +5 -5
- micrOS/source/{LM_genIO.py → modules/LM_genIO.py} +49 -35
- micrOS/source/modules/LM_haptic.py +111 -0
- micrOS/source/modules/LM_i2c.py +61 -0
- micrOS/source/{LM_i2s_mic.py → modules/LM_i2s_mic.py} +20 -23
- micrOS/source/{LM_ld2410.py → modules/LM_ld2410.py} +3 -3
- micrOS/source/{LM_light_sensor.py → modules/LM_light_sensor.py} +22 -26
- micrOS/source/modules/LM_mh_z19c.py +198 -0
- micrOS/source/modules/LM_neoeffects.py +284 -0
- micrOS/source/{LM_neopixel.py → modules/LM_neopixel.py} +26 -31
- micrOS/source/{LM_oled.py → modules/LM_oled.py} +28 -20
- micrOS/source/{LM_oled_sh1106.py → modules/LM_oled_sh1106.py} +28 -24
- micrOS/source/{LM_oled_ui.py → modules/LM_oled_ui.py} +132 -174
- micrOS/source/modules/LM_pacman.py +320 -0
- micrOS/source/{LM_presence.py → modules/LM_presence.py} +24 -36
- micrOS/source/modules/LM_qmi8658.py +204 -0
- micrOS/source/{LM_rencoder.py → modules/LM_rencoder.py} +40 -11
- micrOS/source/modules/LM_rest.py +81 -0
- micrOS/source/{LM_rgb.py → modules/LM_rgb.py} +25 -34
- micrOS/source/{LM_rgbcct.py → modules/LM_rgbcct.py} +5 -5
- micrOS/source/{LM_roboarm.py → modules/LM_roboarm.py} +37 -45
- micrOS/source/modules/LM_robustness.py +137 -0
- micrOS/source/{LM_rp2w.py → modules/LM_rp2w.py} +3 -0
- micrOS/source/{LM_sdcard.py → modules/LM_sdcard.py} +3 -0
- micrOS/source/{LM_servo.py → modules/LM_servo.py} +4 -4
- micrOS/source/modules/LM_sound_event.py +751 -0
- micrOS/source/{LM_stepper.py → modules/LM_stepper.py} +8 -8
- micrOS/source/{LM_switch.py → modules/LM_switch.py} +21 -18
- micrOS/source/{LM_system.py → modules/LM_system.py} +96 -59
- micrOS/source/modules/LM_tcs3472.py +187 -0
- micrOS/source/modules/LM_telegram.py +388 -0
- micrOS/source/modules/LM_trackball.py +287 -0
- micrOS/source/modules/LM_veml7700.py +159 -0
- micrOS/source/modules/LM_web.py +38 -0
- micrOS/source/urequests.py +204 -91
- {toolkit/workspace/precompiled → micrOS/source/web}/dashboard.html +9 -4
- micrOS/source/web/editor.js +440 -0
- micrOS/source/web/filesui.html +178 -0
- micrOS/source/web/filesui.js +338 -0
- micrOS/source/{index.html → web/index.html} +44 -2
- micrOS/source/web/uapi.js +103 -0
- micrOS/source/web/udashboard.js +129 -0
- micrOS/source/web/ustyle.css +55 -0
- micrOS/source/web/uwidgets.js +172 -0
- micrOS/source/web/uwidgets_pro.js +99 -0
- micrOS/utests/__init__.py +0 -0
- micrOS/utests/test_scheduler.py +435 -0
- {micrOSDevToolKit-2.1.5.data → microsdevtoolkit-2.26.1.data}/scripts/devToolKit.py +47 -4
- {micrOSDevToolKit-2.1.5.dist-info → microsdevtoolkit-2.26.1.dist-info}/METADATA +392 -279
- microsdevtoolkit-2.26.1.dist-info/RECORD +396 -0
- {micrOSDevToolKit-2.1.5.dist-info → microsdevtoolkit-2.26.1.dist-info}/WHEEL +1 -1
- toolkit/DevEnvCompile.py +63 -33
- toolkit/DevEnvOTA.py +72 -22
- toolkit/DevEnvUSB.py +147 -77
- toolkit/Gateway.py +9 -9
- toolkit/LM_to_compile.dat +12 -4
- toolkit/MicrOSDevEnv.py +129 -51
- toolkit/WebRepl.py +73 -0
- toolkit/dashboard_apps/BackupRestore.py +171 -0
- toolkit/dashboard_apps/CCTDemo.py +12 -17
- toolkit/dashboard_apps/CCTTest.py +20 -24
- toolkit/dashboard_apps/CamStream.py +2 -6
- toolkit/dashboard_apps/CatGame.py +14 -16
- toolkit/dashboard_apps/Dimmer.py +11 -21
- toolkit/dashboard_apps/GetVersion.py +11 -19
- toolkit/dashboard_apps/MicrophoneTest.py +2 -7
- toolkit/dashboard_apps/NeoEffectsDemo.py +22 -35
- toolkit/dashboard_apps/NeopixelTest.py +20 -25
- toolkit/dashboard_apps/PresenceTest.py +2 -8
- toolkit/dashboard_apps/QMI8685_GYRO.py +68 -0
- toolkit/dashboard_apps/RGBTest.py +20 -24
- toolkit/dashboard_apps/RoboArm.py +24 -32
- toolkit/dashboard_apps/SED_test.py +10 -14
- toolkit/dashboard_apps/SensorsTest.py +61 -0
- toolkit/dashboard_apps/SystemTest.py +219 -117
- toolkit/dashboard_apps/Template_app.py +12 -19
- toolkit/dashboard_apps/_app_base.py +34 -0
- toolkit/dashboard_apps/_gyro_visualizer.py +78 -0
- toolkit/dashboard_apps/uLightDemo.py +15 -24
- toolkit/index.html +6 -5
- toolkit/lib/LocalMachine.py +6 -1
- toolkit/lib/MicrosFiles.py +46 -0
- toolkit/lib/Repository.py +64 -0
- toolkit/lib/TerminalColors.py +4 -0
- toolkit/lib/macroScript.py +371 -0
- toolkit/lib/micrOSClient.py +124 -51
- toolkit/lib/micrOSClientHistory.py +156 -0
- toolkit/lib/pip_package_installer.py +31 -4
- toolkit/micrOSdashboard.py +16 -21
- toolkit/micrOSlint.py +28 -10
- toolkit/simulator_lib/.DS_Store +0 -0
- micrOS/source/IO_esp32.py → toolkit/simulator_lib/IO_darwin.py +3 -0
- toolkit/simulator_lib/__pycache__/IO_darwin.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/aioespnow.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/camera.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/framebuf.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/machine.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/micropython.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/mip.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/neopixel.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/network.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/sim_common.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/simgc.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/simulator.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/uasyncio.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/uos.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/urandom.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/usocket.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/ussl.cpython-312.pyc +0 -0
- toolkit/simulator_lib/aioespnow.py +28 -0
- toolkit/simulator_lib/camera.py +84 -0
- toolkit/simulator_lib/dht.py +1 -1
- toolkit/simulator_lib/framebuf.py +49 -1
- toolkit/simulator_lib/machine.py +32 -2
- toolkit/simulator_lib/micropython.py +3 -3
- toolkit/simulator_lib/mip.py +165 -0
- toolkit/simulator_lib/neopixel.py +3 -2
- toolkit/simulator_lib/network.py +2 -1
- toolkit/simulator_lib/node_config.json +2 -3
- toolkit/simulator_lib/ntptime.py +1 -1
- toolkit/simulator_lib/{sim_console.py → sim_common.py} +2 -3
- toolkit/simulator_lib/simgc.py +6 -2
- toolkit/simulator_lib/simulator.py +138 -46
- toolkit/simulator_lib/uasyncio.py +34 -3
- toolkit/simulator_lib/uos.py +147 -0
- toolkit/simulator_lib/urandom.py +4 -0
- toolkit/simulator_lib/usocket.py +5 -1
- toolkit/simulator_lib/view01.jpg +0 -0
- toolkit/simulator_lib/view02.jpg +0 -0
- toolkit/socketClient.py +43 -23
- toolkit/user_data/webhooks/generic.py +1 -1
- toolkit/user_data/webhooks/macro.py +44 -0
- toolkit/user_data/webhooks/template.macro +20 -0
- toolkit/user_data/webhooks/template.py +1 -1
- toolkit/workspace/precompiled/Auth.mpy +0 -0
- toolkit/workspace/precompiled/Common.mpy +0 -0
- toolkit/workspace/precompiled/Config.mpy +0 -0
- toolkit/workspace/precompiled/Debug.mpy +0 -0
- toolkit/workspace/precompiled/Espnow.mpy +0 -0
- toolkit/workspace/precompiled/Files.mpy +0 -0
- toolkit/workspace/precompiled/Hooks.mpy +0 -0
- toolkit/workspace/precompiled/InterConnect.mpy +0 -0
- toolkit/workspace/precompiled/Interrupts.mpy +0 -0
- toolkit/workspace/precompiled/Logger.mpy +0 -0
- toolkit/workspace/precompiled/Network.mpy +0 -0
- toolkit/workspace/precompiled/Notify.mpy +0 -0
- toolkit/workspace/precompiled/Pacman.mpy +0 -0
- toolkit/workspace/precompiled/Scheduler.mpy +0 -0
- toolkit/workspace/precompiled/Server.mpy +0 -0
- toolkit/workspace/precompiled/Shell.mpy +0 -0
- toolkit/workspace/precompiled/Tasks.mpy +0 -0
- toolkit/workspace/precompiled/Time.mpy +0 -0
- toolkit/workspace/precompiled/Types.mpy +0 -0
- toolkit/workspace/precompiled/Web.mpy +0 -0
- toolkit/workspace/precompiled/_mpy.version +1 -1
- toolkit/workspace/precompiled/config/_git.keep +0 -0
- toolkit/workspace/precompiled/helpers.mpy +0 -0
- toolkit/workspace/precompiled/micrOS.mpy +0 -0
- toolkit/workspace/precompiled/micrOSloader.mpy +0 -0
- toolkit/workspace/precompiled/microIO.mpy +0 -0
- toolkit/workspace/precompiled/modules/IO_esp32.mpy +0 -0
- toolkit/workspace/precompiled/modules/IO_esp32c3.mpy +0 -0
- toolkit/workspace/precompiled/modules/IO_esp32c6.mpy +0 -0
- toolkit/workspace/precompiled/modules/IO_esp32s2.mpy +0 -0
- toolkit/workspace/precompiled/modules/IO_esp32s3.mpy +0 -0
- toolkit/workspace/precompiled/modules/IO_m5stamp.mpy +0 -0
- toolkit/workspace/precompiled/modules/IO_qtpy.mpy +0 -0
- toolkit/workspace/precompiled/modules/IO_rp2.mpy +0 -0
- toolkit/workspace/precompiled/modules/IO_tinypico.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_L298N.mpy +0 -0
- {micrOS/source → toolkit/workspace/precompiled/modules}/LM_L9110_DCmotor.py +4 -4
- toolkit/workspace/precompiled/modules/LM_OV2640.mpy +0 -0
- toolkit/workspace/precompiled/{LM_VL53L0X.py → modules/LM_VL53L0X.py} +5 -5
- toolkit/workspace/precompiled/modules/LM_aht10.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_bme280.mpy +0 -0
- toolkit/workspace/precompiled/{LM_buzzer.mpy → modules/LM_buzzer.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_cct.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_cluster.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_co2.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_dht11.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_dht22.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_ds18.mpy +0 -0
- toolkit/workspace/precompiled/{LM_esp32.py → modules/LM_esp32.py} +16 -4
- toolkit/workspace/precompiled/modules/LM_espnow.py +53 -0
- toolkit/workspace/precompiled/modules/LM_fileserver.mpy +0 -0
- toolkit/workspace/precompiled/{LM_gameOfLife.mpy → modules/LM_gameOfLife.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_genIO.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_haptic.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_i2c.py +61 -0
- toolkit/workspace/precompiled/modules/LM_i2s_mic.mpy +0 -0
- toolkit/workspace/precompiled/{LM_ld2410.mpy → modules/LM_ld2410.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_light_sensor.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_mh_z19c.py +198 -0
- toolkit/workspace/precompiled/modules/LM_neoeffects.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_neopixel.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_oled.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_oled_sh1106.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_oled_ui.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_qmi8658.py +204 -0
- toolkit/workspace/precompiled/{LM_rencoder.py → modules/LM_rencoder.py} +40 -11
- toolkit/workspace/precompiled/modules/LM_rest.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_rgb.mpy +0 -0
- toolkit/workspace/precompiled/{LM_rgbcct.mpy → modules/LM_rgbcct.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_roboarm.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_robustness.py +137 -0
- toolkit/workspace/precompiled/{LM_rp2w.py → modules/LM_rp2w.py} +3 -0
- toolkit/workspace/precompiled/{LM_sdcard.py → modules/LM_sdcard.py} +3 -0
- toolkit/workspace/precompiled/{LM_servo.mpy → modules/LM_servo.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_sound_event.mpy +0 -0
- toolkit/workspace/precompiled/{LM_stepper.mpy → modules/LM_stepper.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_switch.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_system.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_tcs3472.py +187 -0
- toolkit/workspace/precompiled/modules/LM_telegram.mpy +0 -0
- toolkit/workspace/precompiled/{LM_tinyrgb.mpy → modules/LM_tinyrgb.mpy} +0 -0
- toolkit/workspace/precompiled/modules/LM_trackball.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_veml7700.mpy +0 -0
- toolkit/workspace/precompiled/modules/LM_web.mpy +0 -0
- toolkit/workspace/precompiled/urequests.mpy +0 -0
- {micrOS/source → toolkit/workspace/precompiled/web}/dashboard.html +9 -4
- toolkit/workspace/precompiled/web/editor.js +440 -0
- toolkit/workspace/precompiled/web/filesui.html +178 -0
- toolkit/workspace/precompiled/web/filesui.js +338 -0
- toolkit/workspace/precompiled/{index.html → web/index.html} +44 -2
- toolkit/workspace/precompiled/web/uapi.js +103 -0
- toolkit/workspace/precompiled/web/udashboard.js +129 -0
- toolkit/workspace/precompiled/web/ustyle.css +55 -0
- toolkit/workspace/precompiled/web/uwidgets.js +172 -0
- toolkit/workspace/precompiled/web/uwidgets_pro.js +99 -0
- env/driver_cp210x/CH34XSER_MAC/CH34X_DRV_INSTALL_INSTRUCTIONS.pdf +0 -0
- env/driver_cp210x/CH34XSER_MAC/CH34xVCPDriver.pkg +0 -0
- micrOS/micropython/esp32-20231005-v1.21.0.bin +0 -0
- micrOS/micropython/esp32c3-GENERIC-20240105-v1.22.1.bin +0 -0
- micrOS/micropython/esp32c3-GENERIC-20240222-v1.22.2.bin +0 -0
- micrOS/micropython/esp32s2-GENERIC-20240105-v1.22.1.bin +0 -0
- micrOS/micropython/esp32s2-LOLIN_MINI-20220618-v1.19.1.bin +0 -0
- micrOS/micropython/esp32s3-GENERIC-20240105-v1.22.1.bin +0 -0
- micrOS/micropython/esp32s3_spiram_oct-20231005-v1.21.0.bin +0 -0
- micrOS/micropython/rpi-pico-w-20231005-v1.21.0.uf2 +0 -0
- micrOS/micropython/tinypico-20231005-v1.21.0.bin +0 -0
- micrOS/micropython/tinypico-usbc-UM-20240105-v1.22.1.bin +0 -0
- micrOS/source/LM_L298N_DCmotor.py +0 -86
- micrOS/source/LM_catgame.py +0 -74
- micrOS/source/LM_dashboard_be.py +0 -37
- micrOS/source/LM_demo.py +0 -85
- micrOS/source/LM_distance.py +0 -88
- micrOS/source/LM_i2c.py +0 -44
- micrOS/source/LM_intercon.py +0 -57
- micrOS/source/LM_keychain.py +0 -318
- micrOS/source/LM_lmpacman.py +0 -126
- micrOS/source/LM_neoeffects.py +0 -327
- micrOS/source/LM_pet_feeder.py +0 -76
- micrOS/source/LM_ph_sensor.py +0 -51
- micrOS/source/LM_rest.py +0 -40
- micrOS/source/LM_robustness.py +0 -73
- micrOS/source/LM_telegram.py +0 -96
- micrOS/source/reset.py +0 -11
- micrOS/source/uapi.js +0 -76
- micrOS/source/udashboard.js +0 -137
- micrOS/source/ustyle.css +0 -28
- micrOS/source/uwidgets.js +0 -179
- micrOSDevToolKit-2.1.5.dist-info/RECORD +0 -337
- toolkit/dashboard_apps/AirQualityBME280.py +0 -36
- toolkit/dashboard_apps/AirQualityDHT22_CO2.py +0 -36
- toolkit/lib/file_extensions.py +0 -16
- toolkit/simulator_lib/__pycache__/LP_darwin.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/LP_darwin.cpython-38.pyc +0 -0
- toolkit/simulator_lib/__pycache__/LP_darwin.cpython-39.pyc +0 -0
- toolkit/simulator_lib/__pycache__/sim_console.cpython-312.pyc +0 -0
- toolkit/simulator_lib/__pycache__/sim_console.cpython-38.pyc +0 -0
- toolkit/simulator_lib/__pycache__/sim_console.cpython-39.pyc +0 -0
- toolkit/workspace/precompiled/IO_esp32.mpy +0 -0
- toolkit/workspace/precompiled/IO_esp32c3.mpy +0 -0
- toolkit/workspace/precompiled/IO_esp32s2.mpy +0 -0
- toolkit/workspace/precompiled/IO_esp32s3.mpy +0 -0
- toolkit/workspace/precompiled/IO_qtpy.mpy +0 -0
- toolkit/workspace/precompiled/IO_rp2.mpy +0 -0
- toolkit/workspace/precompiled/IO_tinypico.mpy +0 -0
- toolkit/workspace/precompiled/LM_L298N_DCmotor.mpy +0 -0
- toolkit/workspace/precompiled/LM_OV2640.mpy +0 -0
- toolkit/workspace/precompiled/LM_aht10.mpy +0 -0
- toolkit/workspace/precompiled/LM_bme280.mpy +0 -0
- toolkit/workspace/precompiled/LM_catgame.py +0 -74
- toolkit/workspace/precompiled/LM_cct.mpy +0 -0
- toolkit/workspace/precompiled/LM_co2.mpy +0 -0
- toolkit/workspace/precompiled/LM_dashboard_be.py +0 -37
- toolkit/workspace/precompiled/LM_demo.py +0 -85
- toolkit/workspace/precompiled/LM_dht11.mpy +0 -0
- toolkit/workspace/precompiled/LM_dht22.mpy +0 -0
- toolkit/workspace/precompiled/LM_dimmer.mpy +0 -0
- toolkit/workspace/precompiled/LM_distance.py +0 -88
- toolkit/workspace/precompiled/LM_ds18.mpy +0 -0
- toolkit/workspace/precompiled/LM_genIO.mpy +0 -0
- toolkit/workspace/precompiled/LM_i2c.py +0 -44
- toolkit/workspace/precompiled/LM_i2s_mic.mpy +0 -0
- toolkit/workspace/precompiled/LM_intercon.mpy +0 -0
- toolkit/workspace/precompiled/LM_keychain.mpy +0 -0
- toolkit/workspace/precompiled/LM_light_sensor.mpy +0 -0
- toolkit/workspace/precompiled/LM_lmpacman.mpy +0 -0
- toolkit/workspace/precompiled/LM_neoeffects.mpy +0 -0
- toolkit/workspace/precompiled/LM_neopixel.mpy +0 -0
- toolkit/workspace/precompiled/LM_oled.mpy +0 -0
- toolkit/workspace/precompiled/LM_oled_sh1106.mpy +0 -0
- toolkit/workspace/precompiled/LM_oled_ui.mpy +0 -0
- toolkit/workspace/precompiled/LM_pet_feeder.py +0 -76
- toolkit/workspace/precompiled/LM_ph_sensor.py +0 -51
- toolkit/workspace/precompiled/LM_presence.mpy +0 -0
- toolkit/workspace/precompiled/LM_rest.mpy +0 -0
- toolkit/workspace/precompiled/LM_rgb.mpy +0 -0
- toolkit/workspace/precompiled/LM_roboarm.mpy +0 -0
- toolkit/workspace/precompiled/LM_robustness.py +0 -73
- toolkit/workspace/precompiled/LM_switch.mpy +0 -0
- toolkit/workspace/precompiled/LM_system.mpy +0 -0
- toolkit/workspace/precompiled/LM_telegram.mpy +0 -0
- toolkit/workspace/precompiled/reset.mpy +0 -0
- toolkit/workspace/precompiled/uapi.js +0 -76
- toolkit/workspace/precompiled/udashboard.js +0 -137
- toolkit/workspace/precompiled/ustyle.css +0 -28
- toolkit/workspace/precompiled/uwidgets.js +0 -179
- /toolkit/user_data/node_config_archive/.include → /micrOS/source/config/_git.keep +0 -0
- /micrOS/source/{IO_rp2.py → modules/IO_rp2.py} +0 -0
- /micrOS/source/{LM_tinyrgb.py → modules/LM_tinyrgb.py} +0 -0
- {micrOSDevToolKit-2.1.5.dist-info → microsdevtoolkit-2.26.1.dist-info/licenses}/LICENSE +0 -0
- {micrOSDevToolKit-2.1.5.dist-info → microsdevtoolkit-2.26.1.dist-info}/top_level.txt +0 -0
micrOS/source/Web.py
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module is responsible for webserver environment
|
|
3
|
+
dedicated to micrOS framework.
|
|
4
|
+
Built-in-function:
|
|
5
|
+
- response
|
|
6
|
+
- landing page: index.html
|
|
7
|
+
- rest/ - call load modules, e.x.: system/top
|
|
8
|
+
- file response (.html, .css, .js, .jpeg) - generic file server feature
|
|
9
|
+
- "virtual" endpoints - to reply from script on a defined endpoint
|
|
10
|
+
- stream - stream data (jpeg) function
|
|
11
|
+
|
|
12
|
+
Designed by Marcell Ban aka BxNxM and szeka9 (GitHub)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from re import compile
|
|
16
|
+
from json import dumps, loads
|
|
17
|
+
from uos import stat
|
|
18
|
+
import uasyncio as asyncio
|
|
19
|
+
from Tasks import lm_exec, NativeTask, lm_is_loaded
|
|
20
|
+
from Debug import syslog, console_write
|
|
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)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class WebEngine:
|
|
57
|
+
__slots__ = ["client"]
|
|
58
|
+
ENDPOINTS = {}
|
|
59
|
+
AUTH = cfgget('auth')
|
|
60
|
+
VERSION = "n/a"
|
|
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"
|
|
63
|
+
REQ400 = "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nContent-Length: {len}\r\n\r\n{data}"
|
|
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
|
|
81
|
+
|
|
82
|
+
def __init__(self, client, version):
|
|
83
|
+
self.client = client
|
|
84
|
+
WebEngine.VERSION = version
|
|
85
|
+
|
|
86
|
+
async def a_send(self, response:str, encode:str='utf8'):
|
|
87
|
+
raise NotImplementedError("Child class must implement a_send coroutine.")
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def file_type(path:str):
|
|
91
|
+
"""File dynamic Content-Type handling"""
|
|
92
|
+
default_type = "text/plain"
|
|
93
|
+
# Extract the file extension
|
|
94
|
+
ext = path.rsplit('.', 1)[-1]
|
|
95
|
+
# Return the content type based on the file extension
|
|
96
|
+
return WebEngine.CONTENT_TYPES.get(ext, default_type)
|
|
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
|
|
207
|
+
# [1] REST API GET ENDPOINT [/rest]
|
|
208
|
+
if url.startswith('/rest') and _method == "GET":
|
|
209
|
+
self.client.console("[WebCli] --- /rest ACCEPT")
|
|
210
|
+
try:
|
|
211
|
+
await self.client.a_send(WebEngine.rest(url))
|
|
212
|
+
except Exception as e:
|
|
213
|
+
await self.client.a_send(self.REQ404.format(len=len(str(e)), data=e))
|
|
214
|
+
return True
|
|
215
|
+
# [2] DYNAMIC/USER ENDPOINTS (from Load Modules)
|
|
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
|
|
224
|
+
# [3] HOME/PAGE ENDPOINT(s) [default: / -> /index.html]
|
|
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
|
|
232
|
+
try:
|
|
233
|
+
# SEND RESOURCE CONTENT: HTML, JS, CSS (WebEngine.CONTENT_TYPES)
|
|
234
|
+
await self.file_transfer(resource)
|
|
235
|
+
except OSError:
|
|
236
|
+
await self.client.a_send(self.REQ404.format(len=13, data='404 Not Found'))
|
|
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
|
|
244
|
+
# INVALID/BAD REQUEST
|
|
245
|
+
await self.client.a_send(self.REQ400.format(len=15, data='400 Bad Request'))
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def rest(url):
|
|
250
|
+
resp_schema = {'result': {}, 'state': False}
|
|
251
|
+
cmd = url.replace('/rest', '')
|
|
252
|
+
if len(cmd) > 1:
|
|
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())
|
|
257
|
+
# EXECUTE COMMAND - LoadModule
|
|
258
|
+
if WebEngine.AUTH:
|
|
259
|
+
state, out = lm_exec(cmd, jsonify=True) if lm_is_loaded(cmd[0]) else (True, 'Auth:Protected')
|
|
260
|
+
else:
|
|
261
|
+
state, out = lm_exec(cmd, jsonify=True)
|
|
262
|
+
try:
|
|
263
|
+
resp_schema['result'] = loads(out) # Load again ... hack for embedded json converter...
|
|
264
|
+
except:
|
|
265
|
+
resp_schema['result'] = out
|
|
266
|
+
resp_schema['state'] = state
|
|
267
|
+
else:
|
|
268
|
+
resp_schema['result'] = {"micrOS": WebEngine.VERSION, 'node': cfgget('devfid'), 'auth': WebEngine.AUTH}
|
|
269
|
+
if len(tuple(WebEngine.ENDPOINTS.keys())) > 0:
|
|
270
|
+
resp_schema['result']['usr_endpoints'] = tuple(WebEngine.ENDPOINTS)
|
|
271
|
+
resp_schema['state'] = True
|
|
272
|
+
response = dumps(resp_schema)
|
|
273
|
+
return WebEngine.REQ200.format(dtype='text/html', len=len(response), data=response)
|
|
274
|
+
|
|
275
|
+
async def endpoints(self, url:str, method:str, headers:dict, body:bytes):
|
|
276
|
+
url = url[1:] # Cut first / char
|
|
277
|
+
if url in WebEngine.ENDPOINTS and method in WebEngine.ENDPOINTS[url]: # TODO: support for query parameters
|
|
278
|
+
console_write(f"[WebCli] endpoint: {url}")
|
|
279
|
+
# Registered endpoint was found - exec callback
|
|
280
|
+
try:
|
|
281
|
+
# RESOLVE ENDPOINT CALLBACK
|
|
282
|
+
# dtype:
|
|
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
|
+
|
|
298
|
+
if dtype == 'image/jpeg':
|
|
299
|
+
resp = f"HTTP/1.1 200 OK\r\nContent-Type: {dtype}\r\nContent-Length:{len(data)}\r\n\r\n".encode('ascii') + data
|
|
300
|
+
await self.client.a_send(resp, encode=None)
|
|
301
|
+
elif dtype in ('multipart/x-mixed-replace', 'multipart/form-data'):
|
|
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)
|
|
304
|
+
# Start Native stream async task
|
|
305
|
+
task = NativeTask()
|
|
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)
|
|
308
|
+
else: # dtype: text/html or text/plain
|
|
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))
|
|
314
|
+
except Exception as 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)
|
|
319
|
+
return True # Registered endpoint was found and executed
|
|
320
|
+
return False # Not registered endpoint
|
|
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
|
+
|
|
354
|
+
async def stream(self, callback, task, content_type):
|
|
355
|
+
"""
|
|
356
|
+
Async stream method.
|
|
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;*
|
|
360
|
+
"""
|
|
361
|
+
is_coroutine = 'generator' in str(type(callback)) # async function callback auto-detect
|
|
362
|
+
with task:
|
|
363
|
+
task.out = 'Stream started'
|
|
364
|
+
data_to_send = b''
|
|
365
|
+
|
|
366
|
+
while self.client.connected and data_to_send is not None:
|
|
367
|
+
data_to_send = await callback() if is_coroutine else callback()
|
|
368
|
+
part = (f"\r\n--micrOS_boundary\r\nContent-Type: {content_type}\r\n\r\n").encode('ascii') + data_to_send
|
|
369
|
+
task.out = 'Data sent'
|
|
370
|
+
await self.client.a_send(part, encode=None)
|
|
371
|
+
await asyncio.sleep_ms(10)
|
|
372
|
+
|
|
373
|
+
# Gracefully terminate the stream
|
|
374
|
+
if self.client.connected:
|
|
375
|
+
closing_boundary = '\r\n--micrOS_boundary--\r\n'
|
|
376
|
+
await self.client.a_send(closing_boundary)
|
|
377
|
+
await self.client.close()
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
micrOS/source/helpers.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MICROPYTHON REPL micrOS Cli over UART
|
|
3
|
+
- reset() - reboot board
|
|
4
|
+
- shell() - start async micrOS shell (same as ShellCli)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from time import sleep
|
|
8
|
+
from uos import listdir
|
|
9
|
+
from machine import soft_reset, reset as hard_reset
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from Shell import Shell
|
|
13
|
+
from Tasks import Manager
|
|
14
|
+
from Common import micro_task
|
|
15
|
+
# --- nonblocking stdin line reader (USB serial REPL) ---
|
|
16
|
+
import sys, uselect
|
|
17
|
+
_poll = uselect.poll()
|
|
18
|
+
_poll.register(sys.stdin, uselect.POLLIN)
|
|
19
|
+
_buf = []
|
|
20
|
+
except ImportError as e:
|
|
21
|
+
print(f"!!!Cannot import shell: {e}")
|
|
22
|
+
Shell = None
|
|
23
|
+
|
|
24
|
+
#############################################
|
|
25
|
+
# REBOOT DEVICE #
|
|
26
|
+
#############################################
|
|
27
|
+
|
|
28
|
+
def reset():
|
|
29
|
+
"""
|
|
30
|
+
[HELPER] Reboot board
|
|
31
|
+
"""
|
|
32
|
+
print('Device reboot now, boot micrOSloader...')
|
|
33
|
+
sleep(1)
|
|
34
|
+
|
|
35
|
+
if "main.py" in listdir():
|
|
36
|
+
soft_reset()
|
|
37
|
+
else:
|
|
38
|
+
hard_reset()
|
|
39
|
+
|
|
40
|
+
#############################################
|
|
41
|
+
# SHELL in REPL #
|
|
42
|
+
#############################################
|
|
43
|
+
|
|
44
|
+
def _read_line_nb(echo=True):
|
|
45
|
+
"""
|
|
46
|
+
Non-blocking:
|
|
47
|
+
- returns a full line (str, without trailing newline) when '\n' received
|
|
48
|
+
- returns None if no complete line yet
|
|
49
|
+
Echoes typed characters and supports backspace locally.
|
|
50
|
+
"""
|
|
51
|
+
while _poll.poll(0): # check without waiting
|
|
52
|
+
ch = sys.stdin.read(1)
|
|
53
|
+
if not ch:
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
if ch == '\n': # end-of-line (LF)
|
|
57
|
+
line = ''.join(_buf)
|
|
58
|
+
_buf.clear()
|
|
59
|
+
return line # NOTE: we do NOT print a newline
|
|
60
|
+
if ch == '\r': # ignore CR
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# backspace / delete
|
|
64
|
+
if ch in ('\x08', '\x7f'):
|
|
65
|
+
if _buf and echo:
|
|
66
|
+
_buf.pop()
|
|
67
|
+
sys.stdout.write('\x08 \x08') # erase last char visually
|
|
68
|
+
elif _buf:
|
|
69
|
+
_buf.pop()
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# regular char
|
|
73
|
+
_buf.append(ch)
|
|
74
|
+
if echo:
|
|
75
|
+
sys.stdout.write(ch)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def _shell_task(task_id):
|
|
80
|
+
|
|
81
|
+
class ReplShell(Shell):
|
|
82
|
+
async def a_send(self, msg):
|
|
83
|
+
print(f"\n{msg}", end='')
|
|
84
|
+
|
|
85
|
+
shell_inst = ReplShell()
|
|
86
|
+
with micro_task(task_id) as my_task:
|
|
87
|
+
# Init shell welcome msg in repl mode
|
|
88
|
+
await shell_inst.shell("help")
|
|
89
|
+
# Run shell interpreter
|
|
90
|
+
while True:
|
|
91
|
+
my_task.out = "ShellCli in REPL"
|
|
92
|
+
_msg = _read_line_nb() # NoN Blocking ...
|
|
93
|
+
#_msg = input() # Blocking input handling
|
|
94
|
+
if _msg is not None:
|
|
95
|
+
state = await shell_inst.shell(_msg)
|
|
96
|
+
if not state or _msg.strip() == "exit":
|
|
97
|
+
print(f"\nEXIT SHELL: {_msg} ({state}) [Ctrl-C to return micropython repl]")
|
|
98
|
+
break
|
|
99
|
+
await my_task.feed(sleep_ms=10)
|
|
100
|
+
return f"Shell in REPL stopped...({state})"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def shell():
|
|
104
|
+
"""
|
|
105
|
+
[HELPER] Run Shell in REPL
|
|
106
|
+
"""
|
|
107
|
+
if Shell is None:
|
|
108
|
+
return "Cannot run Shell in REPL, import error"
|
|
109
|
+
# Prepare micrOS Task manager
|
|
110
|
+
aio = Manager()
|
|
111
|
+
# Initiate Shell as repl task
|
|
112
|
+
task_id = "repl.shell"
|
|
113
|
+
aio.create_task(callback=_shell_task(task_id), tag=task_id)
|
|
114
|
+
# Run async main event loop
|
|
115
|
+
aio.run_forever()
|
|
116
|
+
return "Async Main Event Loop Stopped"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "helpers":
|
|
120
|
+
# COMMAND LINE INTERFACE
|
|
121
|
+
print("\nmicrOS REPL Tools\n-----------------")
|
|
122
|
+
print("\t[0] helpers.reset() - reboot board")
|
|
123
|
+
if Shell: print("\t[1] helpers.shell() - Start Shell in REPL")
|
|
124
|
+
CHOICES = (reset, shell)
|
|
125
|
+
CHOICE = None
|
|
126
|
+
try:
|
|
127
|
+
CHOICE = input("Select option: ").strip()
|
|
128
|
+
CHOICE = int(CHOICE)
|
|
129
|
+
# EXEC COMMAND
|
|
130
|
+
CHOICES[CHOICE]()
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Invalid input {CHOICE}: {e}")
|