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.
- 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-20241129-v1.24.1.bin → 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 +167 -163
- micrOS/source/Auth.py +37 -0
- micrOS/source/Common.py +361 -116
- micrOS/source/Config.py +32 -22
- micrOS/source/Debug.py +50 -94
- micrOS/source/Espnow.py +377 -100
- micrOS/source/Files.py +207 -0
- micrOS/source/Hooks.py +48 -20
- micrOS/source/InterConnect.py +126 -42
- micrOS/source/Interrupts.py +6 -6
- micrOS/source/Logger.py +63 -26
- micrOS/source/Network.py +41 -21
- micrOS/source/Notify.py +48 -22
- micrOS/source/Pacman.py +326 -0
- micrOS/source/Scheduler.py +14 -54
- micrOS/source/Server.py +67 -69
- micrOS/source/Shell.py +99 -91
- micrOS/source/Tasks.py +141 -95
- micrOS/source/Time.py +19 -18
- micrOS/source/Types.py +53 -9
- micrOS/source/Web.py +381 -76
- 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/config/_git.keep +0 -0
- micrOS/source/helpers.py +132 -0
- micrOS/source/micrOS.py +17 -7
- micrOS/source/micrOSloader.py +5 -12
- micrOS/source/microIO.py +44 -20
- micrOS/source/modules/IO_esp32c6.py +38 -0
- micrOS/source/{IO_esp32s3.py → modules/IO_esp32s3.py} +37 -1
- micrOS/source/{IO_m5stamp.py → modules/IO_m5stamp.py} +35 -1
- micrOS/source/{IO_qtpy.py → modules/IO_qtpy.py} +22 -17
- micrOS/source/{IO_tinypico.py → modules/IO_tinypico.py} +38 -0
- micrOS/source/modules/LM_L298N.py +161 -0
- {toolkit/workspace/precompiled → micrOS/source/modules}/LM_L9110_DCmotor.py +3 -3
- micrOS/source/{LM_OV2640.py → modules/LM_OV2640.py} +45 -27
- micrOS/source/{LM_VL53L0X.py → modules/LM_VL53L0X.py} +3 -3
- micrOS/source/{LM_aht10.py → modules/LM_aht10.py} +2 -2
- micrOS/source/{LM_bme280.py → modules/LM_bme280.py} +3 -3
- micrOS/source/{LM_buzzer.py → modules/LM_buzzer.py} +18 -25
- micrOS/source/{LM_cct.py → modules/LM_cct.py} +17 -21
- micrOS/source/modules/LM_cluster.py +255 -0
- micrOS/source/{LM_co2.py → modules/LM_co2.py} +3 -3
- micrOS/source/{LM_dht11.py → modules/LM_dht11.py} +2 -2
- micrOS/source/{LM_dht22.py → modules/LM_dht22.py} +2 -2
- micrOS/source/{LM_dimmer.py → modules/LM_dimmer.py} +9 -9
- micrOS/source/{LM_distance.py → modules/LM_distance.py} +4 -6
- micrOS/source/{LM_ds18.py → modules/LM_ds18.py} +2 -2
- micrOS/source/{LM_esp32.py → modules/LM_esp32.py} +5 -0
- micrOS/source/modules/LM_espnow.py +53 -0
- micrOS/source/modules/LM_fileserver.py +265 -0
- micrOS/source/{LM_genIO.py → modules/LM_genIO.py} +52 -37
- micrOS/source/{LM_haptic.py → modules/LM_haptic.py} +2 -2
- {toolkit/workspace/precompiled → micrOS/source/modules}/LM_i2c.py +5 -4
- micrOS/source/{LM_i2s_mic.py → modules/LM_i2s_mic.py} +6 -7
- micrOS/source/{LM_ld2410.py → modules/LM_ld2410.py} +2 -2
- micrOS/source/{LM_light_sensor.py → modules/LM_light_sensor.py} +10 -21
- 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} +19 -23
- micrOS/source/{LM_oled.py → modules/LM_oled.py} +2 -2
- micrOS/source/{LM_oled_sh1106.py → modules/LM_oled_sh1106.py} +3 -3
- micrOS/source/{LM_oled_ui.py → modules/LM_oled_ui.py} +72 -64
- micrOS/source/modules/LM_pacman.py +320 -0
- micrOS/source/{LM_presence.py → modules/LM_presence.py} +11 -15
- micrOS/source/modules/LM_qmi8658.py +204 -0
- micrOS/source/{LM_rencoder.py → modules/LM_rencoder.py} +2 -2
- micrOS/source/{LM_rest.py → modules/LM_rest.py} +4 -6
- micrOS/source/{LM_rgb.py → modules/LM_rgb.py} +21 -29
- micrOS/source/{LM_roboarm.py → modules/LM_roboarm.py} +8 -8
- micrOS/source/modules/LM_robustness.py +137 -0
- micrOS/source/{LM_servo.py → modules/LM_servo.py} +3 -3
- micrOS/source/{LM_stepper.py → modules/LM_stepper.py} +5 -5
- micrOS/source/{LM_switch.py → modules/LM_switch.py} +11 -9
- micrOS/source/{LM_system.py → modules/LM_system.py} +38 -32
- micrOS/source/modules/LM_tcs3472.py +187 -0
- micrOS/source/{LM_telegram.py → modules/LM_telegram.py} +164 -116
- micrOS/source/{LM_trackball.py → modules/LM_trackball.py} +3 -3
- micrOS/source/{LM_veml7700.py → modules/LM_veml7700.py} +2 -2
- micrOS/source/modules/LM_web.py +38 -0
- micrOS/source/urequests.py +39 -15
- {toolkit/workspace/precompiled → micrOS/source/web}/dashboard.html +4 -0
- micrOS/source/web/editor.js +440 -0
- micrOS/source/web/filesui.html +178 -0
- micrOS/source/web/filesui.js +338 -0
- {toolkit/workspace/precompiled → micrOS/source/web}/index.html +44 -2
- micrOS/source/{uapi.js → web/uapi.js} +48 -7
- micrOS/source/{ustyle.css → web/ustyle.css} +6 -3
- micrOS/utests/__init__.py +0 -0
- micrOS/utests/test_scheduler.py +435 -0
- {micrOSDevToolKit-2.9.1.data → microsdevtoolkit-2.26.1.data}/scripts/devToolKit.py +33 -3
- {micrOSDevToolKit-2.9.1.dist-info → microsdevtoolkit-2.26.1.dist-info}/METADATA +327 -268
- microsdevtoolkit-2.26.1.dist-info/RECORD +396 -0
- {micrOSDevToolKit-2.9.1.dist-info → microsdevtoolkit-2.26.1.dist-info}/WHEEL +1 -1
- toolkit/DevEnvCompile.py +63 -33
- toolkit/DevEnvOTA.py +58 -22
- toolkit/DevEnvUSB.py +110 -55
- toolkit/Gateway.py +6 -6
- toolkit/LM_to_compile.dat +6 -4
- toolkit/MicrOSDevEnv.py +127 -57
- toolkit/WebRepl.py +73 -0
- toolkit/dashboard_apps/BackupRestore.py +20 -35
- 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 +1 -6
- 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 +202 -105
- toolkit/dashboard_apps/Template_app.py +11 -23
- 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 +4 -4
- 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 +6 -0
- toolkit/lib/micrOSClient.py +123 -50
- toolkit/lib/micrOSClientHistory.py +156 -0
- toolkit/lib/pip_package_installer.py +5 -2
- toolkit/micrOSdashboard.py +12 -17
- toolkit/micrOSlint.py +20 -8
- 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__/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/dht.py +1 -1
- toolkit/simulator_lib/framebuf.py +49 -1
- toolkit/simulator_lib/machine.py +17 -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 +137 -59
- toolkit/simulator_lib/uasyncio.py +33 -2
- toolkit/simulator_lib/uos.py +147 -0
- toolkit/simulator_lib/urandom.py +4 -0
- toolkit/socketClient.py +43 -23
- toolkit/user_data/webhooks/generic.py +1 -1
- toolkit/user_data/webhooks/macro.py +1 -1
- 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/{IO_esp32.mpy → modules/IO_esp32.mpy} +0 -0
- toolkit/workspace/precompiled/{IO_esp32c3.mpy → modules/IO_esp32c3.mpy} +0 -0
- toolkit/workspace/precompiled/modules/IO_esp32c6.mpy +0 -0
- toolkit/workspace/precompiled/{IO_esp32s2.mpy → 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 +3 -3
- toolkit/workspace/precompiled/modules/LM_OV2640.mpy +0 -0
- toolkit/workspace/precompiled/{LM_VL53L0X.py → modules/LM_VL53L0X.py} +3 -3
- toolkit/workspace/precompiled/{LM_aht10.mpy → modules/LM_aht10.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_bme280.mpy → 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/{LM_co2.mpy → modules/LM_co2.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_dht11.mpy → modules/LM_dht11.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_dht22.mpy → 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/{LM_ds18.mpy → modules/LM_ds18.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_esp32.py → modules/LM_esp32.py} +5 -0
- 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/{LM_haptic.mpy → modules/LM_haptic.mpy} +0 -0
- {micrOS/source → toolkit/workspace/precompiled/modules}/LM_i2c.py +5 -4
- 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/{LM_oled.mpy → modules/LM_oled.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_oled_sh1106.mpy → 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} +2 -2
- 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_servo.mpy → modules/LM_servo.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_sound_event.mpy → 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/{LM_trackball.mpy → modules/LM_trackball.mpy} +0 -0
- toolkit/workspace/precompiled/{LM_veml7700.mpy → 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 +4 -0
- toolkit/workspace/precompiled/web/editor.js +440 -0
- toolkit/workspace/precompiled/web/filesui.html +178 -0
- toolkit/workspace/precompiled/web/filesui.js +338 -0
- {micrOS/source → toolkit/workspace/precompiled/web}/index.html +44 -2
- toolkit/workspace/precompiled/{uapi.js → web/uapi.js} +48 -7
- toolkit/workspace/precompiled/{ustyle.css → web/ustyle.css} +6 -3
- micrOS/micropython/esp32-20241129-v1.24.1.bin +0 -0
- micrOS/micropython/esp32c3-20240222-v1.22.2.bin +0 -0
- micrOS/micropython/esp32s2-20240602-v1.23.0.bin +0 -0
- micrOS/micropython/esp32s2-LOLIN_MINI-20220618-v1.19.1.bin +0 -0
- micrOS/micropython/esp32s2-LOLIN_MINI-20240602-v1.23.0.bin +0 -0
- micrOS/micropython/esp32s3-20240105-v1.22.1.bin +0 -0
- micrOS/micropython/esp32s3_spiram_oct-20231005-v1.21.0.bin +0 -0
- micrOS/micropython/esp32s3_spiram_oct-20241129-v1.24.1.bin +0 -0
- micrOS/micropython/rpi-pico-w-20241129-v1.24.1.uf2 +0 -0
- micrOS/micropython/tinypico-20241129-v1.24.1.bin +0 -0
- micrOS/source/LM_L298N_DCmotor.py +0 -86
- micrOS/source/LM_catgame.py +0 -75
- micrOS/source/LM_dashboard_be.py +0 -37
- micrOS/source/LM_demo.py +0 -97
- micrOS/source/LM_espnow.py +0 -23
- micrOS/source/LM_intercon.py +0 -57
- micrOS/source/LM_keychain.py +0 -322
- micrOS/source/LM_lmpacman.py +0 -126
- micrOS/source/LM_neoeffects.py +0 -331
- micrOS/source/LM_oledui.py +0 -972
- micrOS/source/LM_pet_feeder.py +0 -78
- micrOS/source/LM_ph_sensor.py +0 -51
- micrOS/source/LM_robustness.py +0 -74
- micrOS/source/reset.py +0 -11
- micrOSDevToolKit-2.9.1.dist-info/RECORD +0 -365
- 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__/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_esp32s3.mpy +0 -0
- toolkit/workspace/precompiled/IO_m5stamp.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_catgame.py +0 -75
- toolkit/workspace/precompiled/LM_cct.mpy +0 -0
- toolkit/workspace/precompiled/LM_dashboard_be.py +0 -37
- toolkit/workspace/precompiled/LM_demo.py +0 -97
- toolkit/workspace/precompiled/LM_dimmer.mpy +0 -0
- toolkit/workspace/precompiled/LM_distance.mpy +0 -0
- toolkit/workspace/precompiled/LM_espnow.py +0 -23
- toolkit/workspace/precompiled/LM_genIO.mpy +0 -0
- 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_ui.mpy +0 -0
- toolkit/workspace/precompiled/LM_oledui.mpy +0 -0
- toolkit/workspace/precompiled/LM_pet_feeder.py +0 -78
- 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 -74
- 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/node_config.json +0 -1
- toolkit/workspace/precompiled/reset.mpy +0 -0
- /micrOS/source/{IO_esp32.py → modules/IO_esp32.py} +0 -0
- /micrOS/source/{IO_esp32c3.py → modules/IO_esp32c3.py} +0 -0
- /micrOS/source/{IO_esp32s2.py → modules/IO_esp32s2.py} +0 -0
- /micrOS/source/{IO_rp2.py → modules/IO_rp2.py} +0 -0
- /micrOS/source/{LM_gameOfLife.py → modules/LM_gameOfLife.py} +0 -0
- /micrOS/source/{LM_rgbcct.py → modules/LM_rgbcct.py} +0 -0
- /micrOS/source/{LM_rp2w.py → modules/LM_rp2w.py} +0 -0
- /micrOS/source/{LM_sdcard.py → modules/LM_sdcard.py} +0 -0
- /micrOS/source/{LM_sound_event.py → modules/LM_sound_event.py} +0 -0
- /micrOS/source/{LM_tinyrgb.py → modules/LM_tinyrgb.py} +0 -0
- /micrOS/source/{udashboard.js → web/udashboard.js} +0 -0
- /micrOS/source/{uwidgets.js → web/uwidgets.js} +0 -0
- /micrOS/source/{uwidgets_pro.js → web/uwidgets_pro.js} +0 -0
- {micrOSDevToolKit-2.9.1.dist-info → microsdevtoolkit-2.26.1.dist-info/licenses}/LICENSE +0 -0
- {micrOSDevToolKit-2.9.1.dist-info → microsdevtoolkit-2.26.1.dist-info}/top_level.txt +0 -0
- /toolkit/workspace/precompiled/{LM_rp2w.py → modules/LM_rp2w.py} +0 -0
- /toolkit/workspace/precompiled/{LM_sdcard.py → modules/LM_sdcard.py} +0 -0
- /toolkit/workspace/precompiled/{udashboard.js → web/udashboard.js} +0 -0
- /toolkit/workspace/precompiled/{uwidgets.js → web/uwidgets.js} +0 -0
- /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
|
|
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
|
|
87
|
-
if tag.isupper():
|
|
88
|
-
|
|
89
|
-
if widgets
|
|
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
|
-
|
|
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
|
-
|
|
135
|
+
syslog(f"[ERR] resolve {tag} help msg: {e}")
|
|
94
136
|
continue
|
|
95
|
-
|
|
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, .
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
70
|
-
self.client.console(f"[WebCli] --- {url} ACCEPT")
|
|
71
|
-
if resource.split('.')[-1] not in
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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':
|
|
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('
|
|
105
|
-
.replace('%E2%80%
|
|
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
|
|
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.
|
|
119
|
-
resp_schema['result']['usr_endpoints'] = tuple(WebEngine.
|
|
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.
|
|
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:
|
|
133
|
-
# task: multipart/x-mixed-replace | multipart/form-data - data: dict
|
|
134
|
-
#
|
|
135
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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('
|
|
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
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|