diff --git a/.cache/clangd/index/Gui.cpp.BDF53DB313293DD9.idx b/.cache/clangd/index/Gui.cpp.BDF53DB313293DD9.idx index 6a9e5e1..5a2df32 100644 Binary files a/.cache/clangd/index/Gui.cpp.BDF53DB313293DD9.idx and b/.cache/clangd/index/Gui.cpp.BDF53DB313293DD9.idx differ diff --git a/.cache/clangd/index/Gui.hpp.694DB9CADAA5C126.idx b/.cache/clangd/index/Gui.hpp.694DB9CADAA5C126.idx index 9fb2c43..baafda5 100644 Binary files a/.cache/clangd/index/Gui.hpp.694DB9CADAA5C126.idx and b/.cache/clangd/index/Gui.hpp.694DB9CADAA5C126.idx differ diff --git a/.cache/clangd/index/KnxWorker.cpp.DE6DF2C0B88ED24E.idx b/.cache/clangd/index/KnxWorker.cpp.DE6DF2C0B88ED24E.idx index 182ae11..dd5fd3a 100644 Binary files a/.cache/clangd/index/KnxWorker.cpp.DE6DF2C0B88ED24E.idx and b/.cache/clangd/index/KnxWorker.cpp.DE6DF2C0B88ED24E.idx differ diff --git a/.cache/clangd/index/KnxWorker.hpp.F9067C35E6DD8F11.idx b/.cache/clangd/index/KnxWorker.hpp.F9067C35E6DD8F11.idx index fe2378c..0b513e4 100644 Binary files a/.cache/clangd/index/KnxWorker.hpp.F9067C35E6DD8F11.idx and b/.cache/clangd/index/KnxWorker.hpp.F9067C35E6DD8F11.idx differ diff --git a/.cache/clangd/index/SdCard.cpp.485103BC6CBAC699.idx b/.cache/clangd/index/SdCard.cpp.485103BC6CBAC699.idx index 3ef0b0c..cd3881d 100644 Binary files a/.cache/clangd/index/SdCard.cpp.485103BC6CBAC699.idx and b/.cache/clangd/index/SdCard.cpp.485103BC6CBAC699.idx differ diff --git a/.cache/clangd/index/SdCard.hpp.4F7BF469B05B7ED5.idx b/.cache/clangd/index/SdCard.hpp.4F7BF469B05B7ED5.idx index 161efbd..6972fe7 100644 Binary files a/.cache/clangd/index/SdCard.hpp.4F7BF469B05B7ED5.idx and b/.cache/clangd/index/SdCard.hpp.4F7BF469B05B7ED5.idx differ diff --git a/.cache/clangd/index/WebServer.cpp.DAE718E08A398A46.idx b/.cache/clangd/index/WebServer.cpp.DAE718E08A398A46.idx index 7b85683..c6b8df7 100644 Binary files a/.cache/clangd/index/WebServer.cpp.DAE718E08A398A46.idx and b/.cache/clangd/index/WebServer.cpp.DAE718E08A398A46.idx differ diff --git a/.cache/clangd/index/WebServer.hpp.32EAADB285B462B6.idx b/.cache/clangd/index/WebServer.hpp.32EAADB285B462B6.idx index f9e1689..d2cf1c6 100644 Binary files a/.cache/clangd/index/WebServer.hpp.32EAADB285B462B6.idx and b/.cache/clangd/index/WebServer.hpp.32EAADB285B462B6.idx differ diff --git a/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx b/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx index eecdb4b..f872fcd 100644 Binary files a/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx and b/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx differ diff --git a/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx b/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx index 21c6830..f41ecf1 100644 Binary files a/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx and b/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx differ diff --git a/.cache/clangd/index/WidgetManager.hpp.8559171B7B8A010C.idx b/.cache/clangd/index/WidgetManager.hpp.8559171B7B8A010C.idx index dd90ab0..212ea47 100644 Binary files a/.cache/clangd/index/WidgetManager.hpp.8559171B7B8A010C.idx and b/.cache/clangd/index/WidgetManager.hpp.8559171B7B8A010C.idx differ diff --git a/.cache/clangd/index/audio_device.c.AF7F3B046C0957C4.idx b/.cache/clangd/index/audio_device.c.AF7F3B046C0957C4.idx new file mode 100644 index 0000000..cb57b12 Binary files /dev/null and b/.cache/clangd/index/audio_device.c.AF7F3B046C0957C4.idx differ diff --git a/.cache/clangd/index/bth_device.c.873696740B7C8D19.idx b/.cache/clangd/index/bth_device.c.873696740B7C8D19.idx new file mode 100644 index 0000000..1e80250 Binary files /dev/null and b/.cache/clangd/index/bth_device.c.873696740B7C8D19.idx differ diff --git a/.cache/clangd/index/cdc.h.AA9F42A959F46125.idx b/.cache/clangd/index/cdc.h.AA9F42A959F46125.idx new file mode 100644 index 0000000..0f40a4b Binary files /dev/null and b/.cache/clangd/index/cdc.h.AA9F42A959F46125.idx differ diff --git a/.cache/clangd/index/cdc_device.c.97BF2CAEDF28C4DB.idx b/.cache/clangd/index/cdc_device.c.97BF2CAEDF28C4DB.idx new file mode 100644 index 0000000..a5845ef Binary files /dev/null and b/.cache/clangd/index/cdc_device.c.97BF2CAEDF28C4DB.idx differ diff --git a/.cache/clangd/index/dcd.h.22A3E76D1F1071EB.idx b/.cache/clangd/index/dcd.h.22A3E76D1F1071EB.idx new file mode 100644 index 0000000..afc7e8b Binary files /dev/null and b/.cache/clangd/index/dcd.h.22A3E76D1F1071EB.idx differ diff --git a/.cache/clangd/index/dcd_dwc2.c.EA12668E199B240C.idx b/.cache/clangd/index/dcd_dwc2.c.EA12668E199B240C.idx new file mode 100644 index 0000000..8b65b48 Binary files /dev/null and b/.cache/clangd/index/dcd_dwc2.c.EA12668E199B240C.idx differ diff --git a/.cache/clangd/index/descriptors_control.c.FEED3E9810D6E4AB.idx b/.cache/clangd/index/descriptors_control.c.FEED3E9810D6E4AB.idx new file mode 100644 index 0000000..188081b Binary files /dev/null and b/.cache/clangd/index/descriptors_control.c.FEED3E9810D6E4AB.idx differ diff --git a/.cache/clangd/index/descriptors_control.h.89B3FC6DB16E58AA.idx b/.cache/clangd/index/descriptors_control.h.89B3FC6DB16E58AA.idx new file mode 100644 index 0000000..acff5ec Binary files /dev/null and b/.cache/clangd/index/descriptors_control.h.89B3FC6DB16E58AA.idx differ diff --git a/.cache/clangd/index/dfu_device.c.E977E4CF976BA9A3.idx b/.cache/clangd/index/dfu_device.c.E977E4CF976BA9A3.idx new file mode 100644 index 0000000..0609612 Binary files /dev/null and b/.cache/clangd/index/dfu_device.c.E977E4CF976BA9A3.idx differ diff --git a/.cache/clangd/index/dfu_rt_device.c.2B180B5E9788CDA4.idx b/.cache/clangd/index/dfu_rt_device.c.2B180B5E9788CDA4.idx new file mode 100644 index 0000000..d998f01 Binary files /dev/null and b/.cache/clangd/index/dfu_rt_device.c.2B180B5E9788CDA4.idx differ diff --git a/.cache/clangd/index/dwc2_common.c.0E00B74601DCDAB0.idx b/.cache/clangd/index/dwc2_common.c.0E00B74601DCDAB0.idx new file mode 100644 index 0000000..929dabe Binary files /dev/null and b/.cache/clangd/index/dwc2_common.c.0E00B74601DCDAB0.idx differ diff --git a/.cache/clangd/index/dwc2_common.h.8AB5E347C802DCD9.idx b/.cache/clangd/index/dwc2_common.h.8AB5E347C802DCD9.idx new file mode 100644 index 0000000..d4fc478 Binary files /dev/null and b/.cache/clangd/index/dwc2_common.h.8AB5E347C802DCD9.idx differ diff --git a/.cache/clangd/index/dwc2_esp32.h.BEE26F4CCE688400.idx b/.cache/clangd/index/dwc2_esp32.h.BEE26F4CCE688400.idx new file mode 100644 index 0000000..00f9b13 Binary files /dev/null and b/.cache/clangd/index/dwc2_esp32.h.BEE26F4CCE688400.idx differ diff --git a/.cache/clangd/index/dwc2_type.h.26FDC14A89C4469B.idx b/.cache/clangd/index/dwc2_type.h.26FDC14A89C4469B.idx new file mode 100644 index 0000000..ec2015d Binary files /dev/null and b/.cache/clangd/index/dwc2_type.h.26FDC14A89C4469B.idx differ diff --git a/.cache/clangd/index/ecm_rndis_device.c.5F219BFB804E7E9E.idx b/.cache/clangd/index/ecm_rndis_device.c.5F219BFB804E7E9E.idx new file mode 100644 index 0000000..bfe24e3 Binary files /dev/null and b/.cache/clangd/index/ecm_rndis_device.c.5F219BFB804E7E9E.idx differ diff --git a/.cache/clangd/index/esp_hosted_host_fw_ver.h.7627B76907D54D09.idx b/.cache/clangd/index/esp_hosted_host_fw_ver.h.7627B76907D54D09.idx index d105d00..94d8d36 100644 Binary files a/.cache/clangd/index/esp_hosted_host_fw_ver.h.7627B76907D54D09.idx and b/.cache/clangd/index/esp_hosted_host_fw_ver.h.7627B76907D54D09.idx differ diff --git a/.cache/clangd/index/hid_device.c.F3E3B9259A02B7B6.idx b/.cache/clangd/index/hid_device.c.F3E3B9259A02B7B6.idx new file mode 100644 index 0000000..c2de0ec Binary files /dev/null and b/.cache/clangd/index/hid_device.c.F3E3B9259A02B7B6.idx differ diff --git a/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx b/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx index cab86d0..a60511c 100644 Binary files a/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx and b/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx differ diff --git a/.cache/clangd/index/mempool.c.A498C115B2827EA2.idx b/.cache/clangd/index/mempool.c.A498C115B2827EA2.idx index eb4d14a..8926a8b 100644 Binary files a/.cache/clangd/index/mempool.c.A498C115B2827EA2.idx and b/.cache/clangd/index/mempool.c.A498C115B2827EA2.idx differ diff --git a/.cache/clangd/index/midi_device.c.C1D4AC48AFEB6895.idx b/.cache/clangd/index/midi_device.c.C1D4AC48AFEB6895.idx new file mode 100644 index 0000000..f2f3577 Binary files /dev/null and b/.cache/clangd/index/midi_device.c.C1D4AC48AFEB6895.idx differ diff --git a/.cache/clangd/index/msc.h.35B533BA58CE6641.idx b/.cache/clangd/index/msc.h.35B533BA58CE6641.idx new file mode 100644 index 0000000..787395c Binary files /dev/null and b/.cache/clangd/index/msc.h.35B533BA58CE6641.idx differ diff --git a/.cache/clangd/index/msc_device.c.771370A82CA429E0.idx b/.cache/clangd/index/msc_device.c.771370A82CA429E0.idx new file mode 100644 index 0000000..11abb8a Binary files /dev/null and b/.cache/clangd/index/msc_device.c.771370A82CA429E0.idx differ diff --git a/.cache/clangd/index/msc_device.h.3A2AC52FC788AF29.idx b/.cache/clangd/index/msc_device.h.3A2AC52FC788AF29.idx new file mode 100644 index 0000000..3bab47e Binary files /dev/null and b/.cache/clangd/index/msc_device.h.3A2AC52FC788AF29.idx differ diff --git a/.cache/clangd/index/msc_storage.h.F05387CBF173ABFD.idx b/.cache/clangd/index/msc_storage.h.F05387CBF173ABFD.idx new file mode 100644 index 0000000..ca24289 Binary files /dev/null and b/.cache/clangd/index/msc_storage.h.F05387CBF173ABFD.idx differ diff --git a/.cache/clangd/index/ncm_device.c.04581A475F6815D1.idx b/.cache/clangd/index/ncm_device.c.04581A475F6815D1.idx new file mode 100644 index 0000000..581905f Binary files /dev/null and b/.cache/clangd/index/ncm_device.c.04581A475F6815D1.idx differ diff --git a/.cache/clangd/index/ndis.h.F1FD71A464D10A0E.idx b/.cache/clangd/index/ndis.h.F1FD71A464D10A0E.idx new file mode 100644 index 0000000..e7d9287 Binary files /dev/null and b/.cache/clangd/index/ndis.h.F1FD71A464D10A0E.idx differ diff --git a/.cache/clangd/index/net_device.h.25F88D3C45366687.idx b/.cache/clangd/index/net_device.h.25F88D3C45366687.idx new file mode 100644 index 0000000..e141a69 Binary files /dev/null and b/.cache/clangd/index/net_device.h.25F88D3C45366687.idx differ diff --git a/.cache/clangd/index/osal.h.7D98BB9A3CE57313.idx b/.cache/clangd/index/osal.h.7D98BB9A3CE57313.idx new file mode 100644 index 0000000..f1132bd Binary files /dev/null and b/.cache/clangd/index/osal.h.7D98BB9A3CE57313.idx differ diff --git a/.cache/clangd/index/osal_freertos.h.B38523DE336A0048.idx b/.cache/clangd/index/osal_freertos.h.B38523DE336A0048.idx new file mode 100644 index 0000000..8f5c06e Binary files /dev/null and b/.cache/clangd/index/osal_freertos.h.B38523DE336A0048.idx differ diff --git a/.cache/clangd/index/rndis_protocol.h.35776890022ED484.idx b/.cache/clangd/index/rndis_protocol.h.35776890022ED484.idx new file mode 100644 index 0000000..9559099 Binary files /dev/null and b/.cache/clangd/index/rndis_protocol.h.35776890022ED484.idx differ diff --git a/.cache/clangd/index/rndis_reports.c.0438802D593EBF27.idx b/.cache/clangd/index/rndis_reports.c.0438802D593EBF27.idx new file mode 100644 index 0000000..3578f1c Binary files /dev/null and b/.cache/clangd/index/rndis_reports.c.0438802D593EBF27.idx differ diff --git a/.cache/clangd/index/serial_if.c.32545F152DA6FDE5.idx b/.cache/clangd/index/serial_if.c.32545F152DA6FDE5.idx index 2882d2e..a814be3 100644 Binary files a/.cache/clangd/index/serial_if.c.32545F152DA6FDE5.idx and b/.cache/clangd/index/serial_if.c.32545F152DA6FDE5.idx differ diff --git a/.cache/clangd/index/storage_sdmmc.c.590394090995911D.idx b/.cache/clangd/index/storage_sdmmc.c.590394090995911D.idx new file mode 100644 index 0000000..0835abb Binary files /dev/null and b/.cache/clangd/index/storage_sdmmc.c.590394090995911D.idx differ diff --git a/.cache/clangd/index/storage_sdmmc.h.AC1BF042302BCC31.idx b/.cache/clangd/index/storage_sdmmc.h.AC1BF042302BCC31.idx new file mode 100644 index 0000000..c5998b3 Binary files /dev/null and b/.cache/clangd/index/storage_sdmmc.h.AC1BF042302BCC31.idx differ diff --git a/.cache/clangd/index/storage_spiflash.c.BFB058D199D2F441.idx b/.cache/clangd/index/storage_spiflash.c.BFB058D199D2F441.idx new file mode 100644 index 0000000..271be28 Binary files /dev/null and b/.cache/clangd/index/storage_spiflash.c.BFB058D199D2F441.idx differ diff --git a/.cache/clangd/index/storage_spiflash.h.0706D514402D1BE8.idx b/.cache/clangd/index/storage_spiflash.h.0706D514402D1BE8.idx new file mode 100644 index 0000000..5c0a03d Binary files /dev/null and b/.cache/clangd/index/storage_spiflash.h.0706D514402D1BE8.idx differ diff --git a/.cache/clangd/index/tinyusb.c.5C682DCB69D2FE17.idx b/.cache/clangd/index/tinyusb.c.5C682DCB69D2FE17.idx new file mode 100644 index 0000000..d7657cb Binary files /dev/null and b/.cache/clangd/index/tinyusb.c.5C682DCB69D2FE17.idx differ diff --git a/.cache/clangd/index/tinyusb.h.794EEE3601819A5C.idx b/.cache/clangd/index/tinyusb.h.794EEE3601819A5C.idx new file mode 100644 index 0000000..1d0f6d4 Binary files /dev/null and b/.cache/clangd/index/tinyusb.h.794EEE3601819A5C.idx differ diff --git a/.cache/clangd/index/tinyusb_default_config.h.D4E05AD0E6E88D99.idx b/.cache/clangd/index/tinyusb_default_config.h.D4E05AD0E6E88D99.idx new file mode 100644 index 0000000..ff4cb2b Binary files /dev/null and b/.cache/clangd/index/tinyusb_default_config.h.D4E05AD0E6E88D99.idx differ diff --git a/.cache/clangd/index/tinyusb_msc.c.31464F38D0032D28.idx b/.cache/clangd/index/tinyusb_msc.c.31464F38D0032D28.idx new file mode 100644 index 0000000..7138387 Binary files /dev/null and b/.cache/clangd/index/tinyusb_msc.c.31464F38D0032D28.idx differ diff --git a/.cache/clangd/index/tinyusb_msc.h.F691256E4BB75045.idx b/.cache/clangd/index/tinyusb_msc.h.F691256E4BB75045.idx new file mode 100644 index 0000000..6013fde Binary files /dev/null and b/.cache/clangd/index/tinyusb_msc.h.F691256E4BB75045.idx differ diff --git a/.cache/clangd/index/tinyusb_task.c.4F298DBC93948AAA.idx b/.cache/clangd/index/tinyusb_task.c.4F298DBC93948AAA.idx new file mode 100644 index 0000000..0a34787 Binary files /dev/null and b/.cache/clangd/index/tinyusb_task.c.4F298DBC93948AAA.idx differ diff --git a/.cache/clangd/index/tinyusb_task.h.989EACF403225020.idx b/.cache/clangd/index/tinyusb_task.h.989EACF403225020.idx new file mode 100644 index 0000000..d8eba82 Binary files /dev/null and b/.cache/clangd/index/tinyusb_task.h.989EACF403225020.idx differ diff --git a/.cache/clangd/index/tusb.c.454A5C292B21C858.idx b/.cache/clangd/index/tusb.c.454A5C292B21C858.idx new file mode 100644 index 0000000..ba2fcbe Binary files /dev/null and b/.cache/clangd/index/tusb.c.454A5C292B21C858.idx differ diff --git a/.cache/clangd/index/tusb.h.4FB99F46C85CC4E8.idx b/.cache/clangd/index/tusb.h.4FB99F46C85CC4E8.idx new file mode 100644 index 0000000..010fa8a Binary files /dev/null and b/.cache/clangd/index/tusb.h.4FB99F46C85CC4E8.idx differ diff --git a/.cache/clangd/index/tusb_common.h.0A109704CC3CAA28.idx b/.cache/clangd/index/tusb_common.h.0A109704CC3CAA28.idx new file mode 100644 index 0000000..47a3c56 Binary files /dev/null and b/.cache/clangd/index/tusb_common.h.0A109704CC3CAA28.idx differ diff --git a/.cache/clangd/index/tusb_compiler.h.1445DFDDCC9E2A53.idx b/.cache/clangd/index/tusb_compiler.h.1445DFDDCC9E2A53.idx new file mode 100644 index 0000000..62a06a3 Binary files /dev/null and b/.cache/clangd/index/tusb_compiler.h.1445DFDDCC9E2A53.idx differ diff --git a/.cache/clangd/index/tusb_config.h.9F38797A39C9C9D5.idx b/.cache/clangd/index/tusb_config.h.9F38797A39C9C9D5.idx new file mode 100644 index 0000000..d1d06a2 Binary files /dev/null and b/.cache/clangd/index/tusb_config.h.9F38797A39C9C9D5.idx differ diff --git a/.cache/clangd/index/tusb_debug.h.C500CCFCB8EAC876.idx b/.cache/clangd/index/tusb_debug.h.C500CCFCB8EAC876.idx new file mode 100644 index 0000000..7a35c60 Binary files /dev/null and b/.cache/clangd/index/tusb_debug.h.C500CCFCB8EAC876.idx differ diff --git a/.cache/clangd/index/tusb_fifo.c.7ED3466C380F385E.idx b/.cache/clangd/index/tusb_fifo.c.7ED3466C380F385E.idx new file mode 100644 index 0000000..17be114 Binary files /dev/null and b/.cache/clangd/index/tusb_fifo.c.7ED3466C380F385E.idx differ diff --git a/.cache/clangd/index/tusb_fifo.h.36D8D0BFC711BB7D.idx b/.cache/clangd/index/tusb_fifo.h.36D8D0BFC711BB7D.idx new file mode 100644 index 0000000..a98202f Binary files /dev/null and b/.cache/clangd/index/tusb_fifo.h.36D8D0BFC711BB7D.idx differ diff --git a/.cache/clangd/index/tusb_mcu.h.56D7E81A60029B2D.idx b/.cache/clangd/index/tusb_mcu.h.56D7E81A60029B2D.idx new file mode 100644 index 0000000..cb6904b Binary files /dev/null and b/.cache/clangd/index/tusb_mcu.h.56D7E81A60029B2D.idx differ diff --git a/.cache/clangd/index/tusb_option.h.2B019640FA516ED4.idx b/.cache/clangd/index/tusb_option.h.2B019640FA516ED4.idx new file mode 100644 index 0000000..c9f1601 Binary files /dev/null and b/.cache/clangd/index/tusb_option.h.2B019640FA516ED4.idx differ diff --git a/.cache/clangd/index/tusb_private.h.D8423E0FAE6C950F.idx b/.cache/clangd/index/tusb_private.h.D8423E0FAE6C950F.idx new file mode 100644 index 0000000..b9416e1 Binary files /dev/null and b/.cache/clangd/index/tusb_private.h.D8423E0FAE6C950F.idx differ diff --git a/.cache/clangd/index/tusb_types.h.EB765862FAB152B5.idx b/.cache/clangd/index/tusb_types.h.EB765862FAB152B5.idx new file mode 100644 index 0000000..3d97434 Binary files /dev/null and b/.cache/clangd/index/tusb_types.h.EB765862FAB152B5.idx differ diff --git a/.cache/clangd/index/tusb_verify.h.D32763C60C41FB34.idx b/.cache/clangd/index/tusb_verify.h.D32763C60C41FB34.idx new file mode 100644 index 0000000..b92a360 Binary files /dev/null and b/.cache/clangd/index/tusb_verify.h.D32763C60C41FB34.idx differ diff --git a/.cache/clangd/index/usb_descriptors.c.9CC77C8373106E12.idx b/.cache/clangd/index/usb_descriptors.c.9CC77C8373106E12.idx new file mode 100644 index 0000000..e2b9576 Binary files /dev/null and b/.cache/clangd/index/usb_descriptors.c.9CC77C8373106E12.idx differ diff --git a/.cache/clangd/index/usb_descriptors.h.A0C7A9DB4C042C40.idx b/.cache/clangd/index/usb_descriptors.h.A0C7A9DB4C042C40.idx new file mode 100644 index 0000000..7992127 Binary files /dev/null and b/.cache/clangd/index/usb_descriptors.h.A0C7A9DB4C042C40.idx differ diff --git a/.cache/clangd/index/usbd.c.7F1DD9AA33B4C2D4.idx b/.cache/clangd/index/usbd.c.7F1DD9AA33B4C2D4.idx new file mode 100644 index 0000000..ccf47e3 Binary files /dev/null and b/.cache/clangd/index/usbd.c.7F1DD9AA33B4C2D4.idx differ diff --git a/.cache/clangd/index/usbd.h.56783067A4DC8DAE.idx b/.cache/clangd/index/usbd.h.56783067A4DC8DAE.idx new file mode 100644 index 0000000..13d6a3e Binary files /dev/null and b/.cache/clangd/index/usbd.h.56783067A4DC8DAE.idx differ diff --git a/.cache/clangd/index/usbd_control.c.2A74DE2F3283A287.idx b/.cache/clangd/index/usbd_control.c.2A74DE2F3283A287.idx new file mode 100644 index 0000000..b890bfa Binary files /dev/null and b/.cache/clangd/index/usbd_control.c.2A74DE2F3283A287.idx differ diff --git a/.cache/clangd/index/usbd_pvt.h.2CD35CB6E479E150.idx b/.cache/clangd/index/usbd_pvt.h.2CD35CB6E479E150.idx new file mode 100644 index 0000000..86b16ec Binary files /dev/null and b/.cache/clangd/index/usbd_pvt.h.2CD35CB6E479E150.idx differ diff --git a/.cache/clangd/index/usbtmc_device.c.814CA61A9A3C3AB4.idx b/.cache/clangd/index/usbtmc_device.c.814CA61A9A3C3AB4.idx new file mode 100644 index 0000000..750aa58 Binary files /dev/null and b/.cache/clangd/index/usbtmc_device.c.814CA61A9A3C3AB4.idx differ diff --git a/.cache/clangd/index/vendor_device.c.1FAA1FAF6262F519.idx b/.cache/clangd/index/vendor_device.c.1FAA1FAF6262F519.idx new file mode 100644 index 0000000..2894bf2 Binary files /dev/null and b/.cache/clangd/index/vendor_device.c.1FAA1FAF6262F519.idx differ diff --git a/.cache/clangd/index/video_device.c.106B56EA6D6CFC43.idx b/.cache/clangd/index/video_device.c.106B56EA6D6CFC43.idx new file mode 100644 index 0000000..badadc8 Binary files /dev/null and b/.cache/clangd/index/video_device.c.106B56EA6D6CFC43.idx differ diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 9d41746..386a88b 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,11 @@ -idf_component_register(SRCS "KnxWorker.cpp" "Nvs.cpp" "main.cpp" "Display.cpp" "Touch.cpp" "Gui.cpp" "Wifi.cpp" "LvglIdle.c" "Gui/WifiSetting.cpp" "Gui/EthSetting.cpp" "Hardware/Eth.cpp" "WidgetManager.cpp" "WebServer.cpp" "SdCard.cpp" +idf_component_register(SRCS "KnxWorker.cpp" "Nvs.cpp" "main.cpp" "Display.cpp" "Touch.cpp" "Gui.cpp" "Wifi.cpp" "LvglIdle.c" "Gui/WifiSetting.cpp" "Gui/EthSetting.cpp" "Hardware/Eth.cpp" "WidgetManager.cpp" "SdCard.cpp" + "webserver/WebServer.cpp" + "webserver/StaticFileHandlers.cpp" + "webserver/ConfigHandlers.cpp" + "webserver/KnxHandlers.cpp" + "webserver/StatusHandlers.cpp" + "webserver/FileManagerHandlers.cpp" PRIV_REQUIRES spi_flash esp_driver_ppa esp_lcd usb REQUIRES esp_mm esp_eth esp_driver_ppa esp_timer lvgl knx ethernet_init esp_wifi_remote esp_netif esp_event nvs_flash esp_http_server fatfs sdmmc json tinyusb - INCLUDE_DIRS "") + INCLUDE_DIRS "webserver" + EMBED_TXTFILES "embedded/filemanager.html") diff --git a/main/WebServer.cpp b/main/WebServer.cpp deleted file mode 100644 index 044acfa..0000000 --- a/main/WebServer.cpp +++ /dev/null @@ -1,374 +0,0 @@ -#include "WebServer.hpp" -#include "SdCard.hpp" -#include "WidgetManager.hpp" -#include "KnxWorker.hpp" -#include "Gui.hpp" -#include "esp_log.h" -#include "esp_system.h" -#include -#include -#include - -static const char* TAG = "WebServer"; - -WebServer& WebServer::instance() { - static WebServer instance; - return instance; -} - -WebServer::~WebServer() { - stop(); -} - -void WebServer::start() { - if (server_ != nullptr) { - ESP_LOGW(TAG, "Server already running"); - return; - } - - httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - config.uri_match_fn = httpd_uri_match_wildcard; - config.stack_size = 8192; - config.max_uri_handlers = 8; - - ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port); - - if (httpd_start(&server_, &config) != ESP_OK) { - ESP_LOGE(TAG, "Failed to start HTTP server"); - return; - } - - // GET / - Web page (loads index.html from SD card) - httpd_uri_t root = { - .uri = "/", - .method = HTTP_GET, - .handler = rootHandler, - .user_ctx = nullptr - }; - httpd_register_uri_handler(server_, &root); - - // GET /webseite/* - Static files from SD card - httpd_uri_t webseite = { - .uri = "/webseite/*", - .method = HTTP_GET, - .handler = staticFileHandler, - .user_ctx = nullptr - }; - httpd_register_uri_handler(server_, &webseite); - - // GET /images/* - Images from SD card - httpd_uri_t images = { - .uri = "/images/*", - .method = HTTP_GET, - .handler = imagesHandler, - .user_ctx = nullptr - }; - httpd_register_uri_handler(server_, &images); - - // GET /api/config - Get full configuration - httpd_uri_t getConfig = { - .uri = "/api/config", - .method = HTTP_GET, - .handler = getConfigHandler, - .user_ctx = nullptr - }; - httpd_register_uri_handler(server_, &getConfig); - - // POST /api/config - Update configuration (from editor) - httpd_uri_t postConfig = { - .uri = "/api/config", - .method = HTTP_POST, - .handler = postConfigHandler, - .user_ctx = nullptr - }; - httpd_register_uri_handler(server_, &postConfig); - - // POST /api/save - Save and apply configuration - httpd_uri_t postSave = { - .uri = "/api/save", - .method = HTTP_POST, - .handler = postSaveHandler, - .user_ctx = nullptr - }; - httpd_register_uri_handler(server_, &postSave); - - // POST /api/reset - Reset to defaults - httpd_uri_t postReset = { - .uri = "/api/reset", - .method = HTTP_POST, - .handler = postResetHandler, - .user_ctx = nullptr - }; - httpd_register_uri_handler(server_, &postReset); - - // GET /api/knx/addresses - Get KNX group addresses - httpd_uri_t getKnxAddresses = { - .uri = "/api/knx/addresses", - .method = HTTP_GET, - .handler = getKnxAddressesHandler, - .user_ctx = nullptr - }; - httpd_register_uri_handler(server_, &getKnxAddresses); - - // POST /api/usb-mode - Enable USB Mass Storage mode - httpd_uri_t postUsbMode = { - .uri = "/api/usb-mode", - .method = HTTP_POST, - .handler = postUsbModeHandler, - .user_ctx = nullptr - }; - httpd_register_uri_handler(server_, &postUsbMode); - - // GET /api/status - Get system status - httpd_uri_t getStatus = { - .uri = "/api/status", - .method = HTTP_GET, - .handler = getStatusHandler, - .user_ctx = nullptr - }; - httpd_register_uri_handler(server_, &getStatus); - - ESP_LOGI(TAG, "HTTP server started successfully"); -} - -void WebServer::stop() { - if (server_ != nullptr) { - httpd_stop(server_); - server_ = nullptr; - ESP_LOGI(TAG, "HTTP server stopped"); - } -} - -const char* WebServer::getContentType(const char* filepath) { - const char* ext = strrchr(filepath, '.'); - if (!ext) return "application/octet-stream"; - - if (strcasecmp(ext, ".html") == 0 || strcasecmp(ext, ".htm") == 0) return "text/html; charset=utf-8"; - if (strcasecmp(ext, ".css") == 0) return "text/css"; - if (strcasecmp(ext, ".js") == 0) return "application/javascript"; - if (strcasecmp(ext, ".json") == 0) return "application/json"; - if (strcasecmp(ext, ".png") == 0) return "image/png"; - if (strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0) return "image/jpeg"; - if (strcasecmp(ext, ".gif") == 0) return "image/gif"; - if (strcasecmp(ext, ".svg") == 0) return "image/svg+xml"; - if (strcasecmp(ext, ".ico") == 0) return "image/x-icon"; - - return "application/octet-stream"; -} - -esp_err_t WebServer::sendFile(httpd_req_t* req, const char* filepath) { - struct stat st; - if (stat(filepath, &st) != 0) { - ESP_LOGE(TAG, "File not found: %s", filepath); - httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found"); - return ESP_FAIL; - } - - FILE* f = fopen(filepath, "r"); - if (!f) { - ESP_LOGE(TAG, "Failed to open file: %s", filepath); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to open file"); - return ESP_FAIL; - } - - httpd_resp_set_type(req, getContentType(filepath)); - - // Send file in chunks - char* chunk = new char[1024]; - if (!chunk) { - fclose(f); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); - return ESP_FAIL; - } - - size_t read; - while ((read = fread(chunk, 1, 1024, f)) > 0) { - if (httpd_resp_send_chunk(req, chunk, read) != ESP_OK) { - delete[] chunk; - fclose(f); - ESP_LOGE(TAG, "Failed to send chunk"); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Send failed"); - return ESP_FAIL; - } - } - - delete[] chunk; - fclose(f); - - // End chunked response - httpd_resp_send_chunk(req, nullptr, 0); - return ESP_OK; -} - -esp_err_t WebServer::rootHandler(httpd_req_t* req) { - if (!SdCard::instance().isMounted()) { - httpd_resp_set_type(req, "text/html; charset=utf-8"); - const char* errorPage = "

SD Card Error

" - "

SD card not mounted. Please insert SD card with /webseite/index.html

"; - httpd_resp_send(req, errorPage, strlen(errorPage)); - return ESP_OK; - } - - return sendFile(req, "/sdcard/webseite/index.html"); -} - -esp_err_t WebServer::staticFileHandler(httpd_req_t* req) { - // URI: /webseite/filename -> /sdcard/webseite/filename - char filepath[CONFIG_HTTPD_MAX_URI_LEN + 8]; - snprintf(filepath, sizeof(filepath), "/sdcard%.*s", - (int)(sizeof(filepath) - 8), req->uri); - - return sendFile(req, filepath); -} - -esp_err_t WebServer::imagesHandler(httpd_req_t* req) { - // URI: /images/filename -> /sdcard/images/filename - char filepath[CONFIG_HTTPD_MAX_URI_LEN + 8]; - snprintf(filepath, sizeof(filepath), "/sdcard%.*s", - (int)(sizeof(filepath) - 8), req->uri); - - return sendFile(req, filepath); -} - -esp_err_t WebServer::getConfigHandler(httpd_req_t* req) { - // Allocate buffer for JSON (widgets can be large) - char* buf = new char[8192]; - if (buf == nullptr) { - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); - return ESP_FAIL; - } - - WidgetManager::instance().getConfigJson(buf, 8192); - - httpd_resp_set_type(req, "application/json"); - httpd_resp_send(req, buf, strlen(buf)); - - delete[] buf; - return ESP_OK; -} - -esp_err_t WebServer::postConfigHandler(httpd_req_t* req) { - // Read request body - int total_len = req->content_len; - if (total_len > 8192) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Content too large"); - return ESP_FAIL; - } - - char* buf = new char[total_len + 1]; - if (buf == nullptr) { - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); - return ESP_FAIL; - } - - int received = 0; - while (received < total_len) { - int ret = httpd_req_recv(req, buf + received, total_len - received); - if (ret <= 0) { - delete[] buf; - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Receive failed"); - return ESP_FAIL; - } - received += ret; - } - buf[received] = '\0'; - - // Update config (does NOT apply yet) - bool success = WidgetManager::instance().updateConfigFromJson(buf); - delete[] buf; - - if (success) { - httpd_resp_set_type(req, "application/json"); - httpd_resp_send(req, "{\"status\":\"ok\"}", 15); - } else { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); - } - - return success ? ESP_OK : ESP_FAIL; -} - -esp_err_t WebServer::postSaveHandler(httpd_req_t* req) { - ESP_LOGI(TAG, "Saving and applying configuration"); - - WidgetManager::instance().saveAndApply(); - - httpd_resp_set_type(req, "application/json"); - httpd_resp_send(req, "{\"status\":\"ok\"}", 15); - return ESP_OK; -} - -esp_err_t WebServer::postResetHandler(httpd_req_t* req) { - ESP_LOGI(TAG, "Resetting to defaults"); - - WidgetManager::instance().resetToDefaults(); - - httpd_resp_set_type(req, "application/json"); - httpd_resp_send(req, "{\"status\":\"ok\"}", 15); - return ESP_OK; -} - -esp_err_t WebServer::getKnxAddressesHandler(httpd_req_t* req) { - char buf[2048]; - int pos = 0; - - pos += snprintf(buf + pos, sizeof(buf) - pos, "["); - - // Get group objects from KnxWorker - KnxWorker& knxWorker = Gui::knxWorker; - size_t count = knxWorker.getGroupObjectCount(); - - for (size_t i = 1; i <= count && pos < (int)sizeof(buf) - 100; i++) { - KnxGroupObjectInfo info; - if (knxWorker.getGroupObjectInfo(i, info)) { - char addrStr[16]; - KnxWorker::formatGroupAddress(info.groupAddress, addrStr, sizeof(addrStr)); - - if (i > 1) pos += snprintf(buf + pos, sizeof(buf) - pos, ","); - - pos += snprintf(buf + pos, sizeof(buf) - pos, - "{\"index\":%d,\"addr\":%d,\"addrStr\":\"%s\",\"comm\":%s,\"read\":%s,\"write\":%s}", - info.goIndex, - info.groupAddress, - addrStr, - info.commFlag ? "true" : "false", - info.readFlag ? "true" : "false", - info.writeFlag ? "true" : "false" - ); - } - } - - snprintf(buf + pos, sizeof(buf) - pos, "]"); - - httpd_resp_set_type(req, "application/json"); - httpd_resp_send(req, buf, strlen(buf)); - return ESP_OK; -} - -esp_err_t WebServer::postUsbModeHandler(httpd_req_t* req) { - ESP_LOGI(TAG, "Enabling USB Mass Storage mode"); - - bool success = SdCard::instance().enableUsbMsc(); - - if (success) { - httpd_resp_set_type(req, "application/json"); - const char* response = "{\"status\":\"ok\",\"message\":\"USB MSC enabled. Connect USB cable to access SD card. Reboot to return to normal mode.\"}"; - httpd_resp_send(req, response, strlen(response)); - } else { - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to enable USB MSC"); - } - - return success ? ESP_OK : ESP_FAIL; -} - -esp_err_t WebServer::getStatusHandler(httpd_req_t* req) { - char buf[256]; - snprintf(buf, sizeof(buf), - "{\"sdMounted\":%s,\"usbMscActive\":%s}", - SdCard::instance().isMounted() ? "true" : "false", - SdCard::instance().isUsbMscActive() ? "true" : "false" - ); - - httpd_resp_set_type(req, "application/json"); - httpd_resp_send(req, buf, strlen(buf)); - return ESP_OK; -} diff --git a/main/WebServer.hpp b/main/WebServer.hpp deleted file mode 100644 index f1ffcd5..0000000 --- a/main/WebServer.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include "esp_http_server.h" - -class WebServer { -public: - static WebServer& instance(); - - void start(); - void stop(); - bool isRunning() const { return server_ != nullptr; } - -private: - WebServer() = default; - ~WebServer(); - WebServer(const WebServer&) = delete; - WebServer& operator=(const WebServer&) = delete; - - httpd_handle_t server_ = nullptr; - - // HTTP handlers - static esp_err_t rootHandler(httpd_req_t* req); - static esp_err_t staticFileHandler(httpd_req_t* req); - static esp_err_t imagesHandler(httpd_req_t* req); - static esp_err_t getConfigHandler(httpd_req_t* req); - static esp_err_t postConfigHandler(httpd_req_t* req); - static esp_err_t postSaveHandler(httpd_req_t* req); - static esp_err_t postResetHandler(httpd_req_t* req); - static esp_err_t getKnxAddressesHandler(httpd_req_t* req); - static esp_err_t postUsbModeHandler(httpd_req_t* req); - static esp_err_t getStatusHandler(httpd_req_t* req); - - // Helper functions - static const char* getContentType(const char* filepath); - static esp_err_t sendFile(httpd_req_t* req, const char* filepath); -}; diff --git a/main/embedded/filemanager.html b/main/embedded/filemanager.html new file mode 100644 index 0000000..90f2704 --- /dev/null +++ b/main/embedded/filemanager.html @@ -0,0 +1,222 @@ + + + + + +SD Card File Manager + + + +
+

SD Card File Manager

+
+
SD Card
+
USB MSC
+
+
+

Dateien hier ablegen oder

+
+
+
+ + + + +
+
Pfad: /
+
Laden...
+
+ + +
+ + + diff --git a/main/webserver/ConfigHandlers.cpp b/main/webserver/ConfigHandlers.cpp new file mode 100644 index 0000000..c9657b4 --- /dev/null +++ b/main/webserver/ConfigHandlers.cpp @@ -0,0 +1,78 @@ +#include "WebServer.hpp" +#include "../WidgetManager.hpp" +#include "esp_log.h" +#include + +static const char* TAG = "WebServer"; + +esp_err_t WebServer::getConfigHandler(httpd_req_t* req) { + char* buf = new char[8192]; + if (buf == nullptr) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); + return ESP_FAIL; + } + + WidgetManager::instance().getConfigJson(buf, 8192); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, buf, strlen(buf)); + + delete[] buf; + return ESP_OK; +} + +esp_err_t WebServer::postConfigHandler(httpd_req_t* req) { + int total_len = req->content_len; + if (total_len > 8192) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Content too large"); + return ESP_FAIL; + } + + char* buf = new char[total_len + 1]; + if (buf == nullptr) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); + return ESP_FAIL; + } + + int received = 0; + while (received < total_len) { + int ret = httpd_req_recv(req, buf + received, total_len - received); + if (ret <= 0) { + delete[] buf; + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Receive failed"); + return ESP_FAIL; + } + received += ret; + } + buf[received] = '\0'; + + bool success = WidgetManager::instance().updateConfigFromJson(buf); + delete[] buf; + + cJSON* json = cJSON_CreateObject(); + if (success) { + cJSON_AddStringToObject(json, "status", "ok"); + } else { + cJSON_AddStringToObject(json, "status", "error"); + cJSON_AddStringToObject(json, "message", "Invalid JSON"); + } + return sendJsonObject(req, json); +} + +esp_err_t WebServer::postSaveHandler(httpd_req_t* req) { + ESP_LOGI(TAG, "Saving and applying configuration"); + WidgetManager::instance().saveAndApply(); + + cJSON* json = cJSON_CreateObject(); + cJSON_AddStringToObject(json, "status", "ok"); + return sendJsonObject(req, json); +} + +esp_err_t WebServer::postResetHandler(httpd_req_t* req) { + ESP_LOGI(TAG, "Resetting to defaults"); + WidgetManager::instance().resetToDefaults(); + + cJSON* json = cJSON_CreateObject(); + cJSON_AddStringToObject(json, "status", "ok"); + return sendJsonObject(req, json); +} diff --git a/main/webserver/FileManagerHandlers.cpp b/main/webserver/FileManagerHandlers.cpp new file mode 100644 index 0000000..b521bc9 --- /dev/null +++ b/main/webserver/FileManagerHandlers.cpp @@ -0,0 +1,392 @@ +#include "WebServer.hpp" +#include "../SdCard.hpp" +#include "esp_log.h" +#include +#include +#include +#include +#include +#include + +static const char* TAG = "WebServer"; + +// Embedded file manager HTML (from Flash) +extern const uint8_t filemanager_html_start[] asm("_binary_filemanager_html_start"); +extern const uint8_t filemanager_html_end[] asm("_binary_filemanager_html_end"); + +// Maximum upload size (2 MB) +static constexpr size_t MAX_UPLOAD_SIZE = 2 * 1024 * 1024; + +// GET /files - Serve embedded file manager HTML +esp_err_t WebServer::fileManagerHandler(httpd_req_t* req) { + httpd_resp_set_type(req, "text/html; charset=utf-8"); + size_t len = filemanager_html_end - filemanager_html_start; + httpd_resp_send(req, (const char*)filemanager_html_start, len); + return ESP_OK; +} + +// GET /api/files/list?path=/ - List directory contents +esp_err_t WebServer::filesListHandler(httpd_req_t* req) { + char path[128] = "/"; + getQueryParam(req, "path", path, sizeof(path)); + + char fullPath[256]; + snprintf(fullPath, sizeof(fullPath), "%.7s%.127s", SdCard::MOUNT_POINT, path); + + if (!SdCard::instance().isMounted()) { + return sendJsonError(req, "SD card not mounted"); + } + + DIR* dir = opendir(fullPath); + if (!dir) { + return sendJsonError(req, "Cannot open directory"); + } + + cJSON* json = cJSON_CreateObject(); + cJSON_AddBoolToObject(json, "success", true); + cJSON_AddStringToObject(json, "path", path); + cJSON* files = cJSON_AddArrayToObject(json, "files"); + + struct dirent* entry; + while ((entry = readdir(dir)) != nullptr) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; + + char entryPath[512]; + size_t fullPathLen = strlen(fullPath); + const char* sep = (fullPathLen > 0 && fullPath[fullPathLen-1] == '/') ? "" : "/"; + snprintf(entryPath, sizeof(entryPath), "%.255s%s%.255s", fullPath, sep, entry->d_name); + + struct stat st; + bool isDir = false; + size_t fileSize = 0; + + if (stat(entryPath, &st) == 0) { + isDir = S_ISDIR(st.st_mode); + fileSize = st.st_size; + } else { + isDir = (entry->d_type == DT_DIR); + } + + cJSON* fileObj = cJSON_CreateObject(); + cJSON_AddStringToObject(fileObj, "name", entry->d_name); + cJSON_AddBoolToObject(fileObj, "isDir", isDir); + cJSON_AddNumberToObject(fileObj, "size", (double)fileSize); + cJSON_AddItemToArray(files, fileObj); + } + + closedir(dir); + return sendJsonObject(req, json); +} + +// POST /api/files/upload?path=/ - Upload file (multipart/form-data) +esp_err_t WebServer::filesUploadHandler(httpd_req_t* req) { + char path[128] = "/"; + getQueryParam(req, "path", path, sizeof(path)); + + if (!SdCard::instance().isMounted()) { + return sendJsonError(req, "SD card not mounted"); + } + + if (req->content_len > MAX_UPLOAD_SIZE) { + return sendJsonError(req, "File too large (max 2MB)"); + } + + char contentType[128] = ""; + if (httpd_req_get_hdr_value_str(req, "Content-Type", contentType, sizeof(contentType)) != ESP_OK) { + return sendJsonError(req, "Missing Content-Type"); + } + + char* boundaryStart = strstr(contentType, "boundary="); + if (!boundaryStart) { + return sendJsonError(req, "Invalid multipart boundary"); + } + boundaryStart += 9; + char boundary[72]; + snprintf(boundary, sizeof(boundary), "--%.68s", boundaryStart); + + size_t contentLen = req->content_len; + char* fullData = new char[contentLen + 1]; + if (!fullData) { + return sendJsonError(req, "Out of memory"); + } + + size_t totalReceived = 0; + while (totalReceived < contentLen) { + int ret = httpd_req_recv(req, fullData + totalReceived, contentLen - totalReceived); + if (ret <= 0) { + delete[] fullData; + return sendJsonError(req, "Receive failed"); + } + totalReceived += ret; + } + fullData[totalReceived] = '\0'; + + char* filenameStart = strstr(fullData, "filename=\""); + if (!filenameStart) { + delete[] fullData; + return sendJsonError(req, "No filename found"); + } + filenameStart += 10; + char* filenameEnd = strchr(filenameStart, '"'); + if (!filenameEnd) { + delete[] fullData; + return sendJsonError(req, "Invalid filename"); + } + + size_t filenameLen = filenameEnd - filenameStart; + char filename[128]; + if (filenameLen >= sizeof(filename)) filenameLen = sizeof(filename) - 1; + memcpy(filename, filenameStart, filenameLen); + filename[filenameLen] = '\0'; + + char* dataStart = strstr(filenameEnd, "\r\n\r\n"); + if (!dataStart) { + delete[] fullData; + return sendJsonError(req, "Invalid multipart format"); + } + dataStart += 4; + + char endBoundary[80]; + snprintf(endBoundary, sizeof(endBoundary), "\r\n%.72s", boundary); + char* dataEnd = strstr(dataStart, endBoundary); + if (!dataEnd) { + dataEnd = strstr(dataStart, boundary); + } + if (!dataEnd) { + delete[] fullData; + return sendJsonError(req, "End boundary not found"); + } + + size_t fileSize = dataEnd - dataStart; + + char resolvedDir[384]; + char targetDirPath[256]; + snprintf(targetDirPath, sizeof(targetDirPath), "%.7s%.127s", SdCard::MOUNT_POINT, path); + + const char* actualDir = targetDirPath; + if (resolveCaseInsensitivePath(targetDirPath, resolvedDir, sizeof(resolvedDir))) { + actualDir = resolvedDir; + } + + char fullPath[384]; + snprintf(fullPath, sizeof(fullPath), "%.250s/%.120s", actualDir, filename); + + ESP_LOGI(TAG, "Uploading: %s (%zu bytes)", fullPath, fileSize); + + struct stat dirStat; + if (stat(actualDir, &dirStat) != 0) { + if (mkdir(actualDir, 0755) != 0 && errno != EEXIST) { + delete[] fullData; + return sendJsonError(req, "Cannot create directory"); + } + } + + FILE* f = fopen(fullPath, "wb"); + if (!f) { + ESP_LOGE(TAG, "Cannot create file: %s (errno=%d)", fullPath, errno); + delete[] fullData; + return sendJsonError(req, "Cannot create file"); + } + + size_t written = fwrite(dataStart, 1, fileSize, f); + fclose(f); + delete[] fullData; + + if (written != fileSize) { + return sendJsonError(req, "Write failed"); + } + + ESP_LOGI(TAG, "Upload complete: %s", fullPath); + + cJSON* json = cJSON_CreateObject(); + cJSON_AddBoolToObject(json, "success", true); + cJSON_AddStringToObject(json, "filename", filename); + cJSON_AddNumberToObject(json, "size", (double)fileSize); + return sendJsonObject(req, json); +} + +// GET /api/files/download?file=/path/to/file - Download file +esp_err_t WebServer::filesDownloadHandler(httpd_req_t* req) { + char file[256] = ""; + if (!getQueryParam(req, "file", file, sizeof(file)) || strlen(file) == 0) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing file parameter"); + return ESP_FAIL; + } + + char fullPath[384]; + snprintf(fullPath, sizeof(fullPath), "%.7s%.255s", SdCard::MOUNT_POINT, file); + + const char* basename = strrchr(file, '/'); + basename = basename ? basename + 1 : file; + char disposition[256]; + snprintf(disposition, sizeof(disposition), "attachment; filename=\"%.200s\"", basename); + httpd_resp_set_hdr(req, "Content-Disposition", disposition); + + return sendFile(req, fullPath); +} + +// DELETE /api/files/delete?file=/path/to/file - Delete file or empty directory +esp_err_t WebServer::filesDeleteHandler(httpd_req_t* req) { + char file[256] = ""; + if (!getQueryParam(req, "file", file, sizeof(file)) || strlen(file) == 0) { + return sendJsonError(req, "Missing file parameter"); + } + + if (!SdCard::instance().isMounted()) { + return sendJsonError(req, "SD card not mounted"); + } + + char fullPath[384]; + snprintf(fullPath, sizeof(fullPath), "%.7s%.255s", SdCard::MOUNT_POINT, file); + + ESP_LOGI(TAG, "Deleting: %s", fullPath); + + struct stat st; + if (stat(fullPath, &st) != 0) { + return sendJsonError(req, "File not found"); + } + + int result; + if (S_ISDIR(st.st_mode)) { + result = rmdir(fullPath); + } else { + result = unlink(fullPath); + } + + if (result == 0) { + return sendJsonSuccess(req); + } + return sendJsonError(req, "Delete failed (directory not empty?)"); +} + +// POST /api/files/mkdir?path=/path/to/new/dir - Create directory +esp_err_t WebServer::filesMkdirHandler(httpd_req_t* req) { + char path[256] = ""; + if (!getQueryParam(req, "path", path, sizeof(path)) || strlen(path) == 0) { + return sendJsonError(req, "Missing path parameter"); + } + + if (!SdCard::instance().isMounted()) { + return sendJsonError(req, "SD card not mounted"); + } + + char fullPath[384]; + snprintf(fullPath, sizeof(fullPath), "%.7s%.255s", SdCard::MOUNT_POINT, path); + + ESP_LOGI(TAG, "Creating directory: %s", fullPath); + + if (mkdir(fullPath, 0755) == 0) { + return sendJsonSuccess(req); + } + return sendJsonError(req, "Cannot create directory"); +} + +// GET /api/files/read?file=/path/to/file - Read text file content for editor +esp_err_t WebServer::filesReadHandler(httpd_req_t* req) { + char file[256] = ""; + if (!getQueryParam(req, "file", file, sizeof(file)) || strlen(file) == 0) { + return sendJsonError(req, "Missing file parameter"); + } + + if (!SdCard::instance().isMounted()) { + return sendJsonError(req, "SD card not mounted"); + } + + char targetPath[384]; + snprintf(targetPath, sizeof(targetPath), "%.7s%.255s", SdCard::MOUNT_POINT, file); + + char resolvedPath[384]; + const char* actualPath = targetPath; + if (resolveCaseInsensitivePath(targetPath, resolvedPath, sizeof(resolvedPath))) { + actualPath = resolvedPath; + } + + struct stat st; + if (stat(actualPath, &st) != 0) { + return sendJsonError(req, "File not found"); + } + + if (st.st_size > 65536) { + return sendJsonError(req, "File too large for editor (max 64KB)"); + } + + FILE* f = fopen(actualPath, "r"); + if (!f) { + return sendJsonError(req, "Cannot open file"); + } + + char* content = new char[st.st_size + 1]; + if (!content) { + fclose(f); + return sendJsonError(req, "Out of memory"); + } + + size_t bytesRead = fread(content, 1, st.st_size, f); + fclose(f); + content[bytesRead] = '\0'; + + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + httpd_resp_send(req, content, bytesRead); + + delete[] content; + return ESP_OK; +} + +// POST /api/files/write?file=/path/to/file - Write text file content from editor +esp_err_t WebServer::filesWriteHandler(httpd_req_t* req) { + char file[256] = ""; + if (!getQueryParam(req, "file", file, sizeof(file)) || strlen(file) == 0) { + return sendJsonError(req, "Missing file parameter"); + } + + if (!SdCard::instance().isMounted()) { + return sendJsonError(req, "SD card not mounted"); + } + + if (req->content_len > 65536) { + return sendJsonError(req, "Content too large (max 64KB)"); + } + + char targetPath[384]; + snprintf(targetPath, sizeof(targetPath), "%.7s%.255s", SdCard::MOUNT_POINT, file); + + char resolvedPath[384]; + const char* actualPath = targetPath; + if (resolveCaseInsensitivePath(targetPath, resolvedPath, sizeof(resolvedPath))) { + actualPath = resolvedPath; + } + + char* content = new char[req->content_len + 1]; + if (!content) { + return sendJsonError(req, "Out of memory"); + } + + size_t received = 0; + while (received < req->content_len) { + int ret = httpd_req_recv(req, content + received, req->content_len - received); + if (ret <= 0) { + delete[] content; + return sendJsonError(req, "Receive failed"); + } + received += ret; + } + content[received] = '\0'; + + ESP_LOGI(TAG, "Writing file: %s (%zu bytes)", actualPath, received); + + FILE* f = fopen(actualPath, "w"); + if (!f) { + delete[] content; + return sendJsonError(req, "Cannot create file"); + } + + size_t written = fwrite(content, 1, received, f); + fclose(f); + delete[] content; + + if (written != received) { + return sendJsonError(req, "Write incomplete"); + } + + return sendJsonSuccess(req); +} diff --git a/main/webserver/KnxHandlers.cpp b/main/webserver/KnxHandlers.cpp new file mode 100644 index 0000000..116743f --- /dev/null +++ b/main/webserver/KnxHandlers.cpp @@ -0,0 +1,35 @@ +#include "WebServer.hpp" +#include "../KnxWorker.hpp" +#include "../Gui.hpp" +#include + +esp_err_t WebServer::getKnxAddressesHandler(httpd_req_t* req) { + KnxWorker& knxWorker = Gui::knxWorker; + size_t count = knxWorker.getGroupObjectCount(); + + cJSON* arr = cJSON_CreateArray(); + + for (size_t i = 1; i <= count; i++) { + KnxGroupObjectInfo info; + if (knxWorker.getGroupObjectInfo(i, info)) { + char addrStr[16]; + KnxWorker::formatGroupAddress(info.groupAddress, addrStr, sizeof(addrStr)); + + cJSON* obj = cJSON_CreateObject(); + cJSON_AddNumberToObject(obj, "index", info.goIndex); + cJSON_AddNumberToObject(obj, "addr", info.groupAddress); + cJSON_AddStringToObject(obj, "addrStr", addrStr); + cJSON_AddBoolToObject(obj, "comm", info.commFlag); + cJSON_AddBoolToObject(obj, "read", info.readFlag); + cJSON_AddBoolToObject(obj, "write", info.writeFlag); + cJSON_AddItemToArray(arr, obj); + } + } + + char* str = cJSON_PrintUnformatted(arr); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, str, strlen(str)); + free(str); + cJSON_Delete(arr); + return ESP_OK; +} diff --git a/main/webserver/StaticFileHandlers.cpp b/main/webserver/StaticFileHandlers.cpp new file mode 100644 index 0000000..e87ff67 --- /dev/null +++ b/main/webserver/StaticFileHandlers.cpp @@ -0,0 +1,35 @@ +#include "WebServer.hpp" +#include "../SdCard.hpp" +#include "esp_log.h" +#include + +static const char* TAG = "WebServer"; + +esp_err_t WebServer::rootHandler(httpd_req_t* req) { + if (!SdCard::instance().isMounted()) { + httpd_resp_set_type(req, "text/html; charset=utf-8"); + const char* errorPage = "

SD Card Error

" + "

SD card not mounted. Please insert SD card with /webseite/index.html

" + "

Open File Manager

"; + httpd_resp_send(req, errorPage, strlen(errorPage)); + return ESP_OK; + } + + return sendFile(req, "/sdcard/webseite/index.html"); +} + +esp_err_t WebServer::staticFileHandler(httpd_req_t* req) { + char filepath[CONFIG_HTTPD_MAX_URI_LEN + 8]; + snprintf(filepath, sizeof(filepath), "/sdcard%.*s", + (int)(sizeof(filepath) - 8), req->uri); + + return sendFile(req, filepath); +} + +esp_err_t WebServer::imagesHandler(httpd_req_t* req) { + char filepath[CONFIG_HTTPD_MAX_URI_LEN + 8]; + snprintf(filepath, sizeof(filepath), "/sdcard%.*s", + (int)(sizeof(filepath) - 8), req->uri); + + return sendFile(req, filepath); +} diff --git a/main/webserver/StatusHandlers.cpp b/main/webserver/StatusHandlers.cpp new file mode 100644 index 0000000..3fd9144 --- /dev/null +++ b/main/webserver/StatusHandlers.cpp @@ -0,0 +1,29 @@ +#include "WebServer.hpp" +#include "../SdCard.hpp" +#include "esp_log.h" +#include + +static const char* TAG = "WebServer"; + +esp_err_t WebServer::postUsbModeHandler(httpd_req_t* req) { + ESP_LOGI(TAG, "Enabling USB Mass Storage mode"); + + bool success = SdCard::instance().enableUsbMsc(); + + cJSON* json = cJSON_CreateObject(); + if (success) { + cJSON_AddStringToObject(json, "status", "ok"); + cJSON_AddStringToObject(json, "message", "USB MSC enabled. Connect USB cable to access SD card. Reboot to return to normal mode."); + } else { + cJSON_AddStringToObject(json, "status", "error"); + cJSON_AddStringToObject(json, "message", "Failed to enable USB MSC"); + } + return sendJsonObject(req, json); +} + +esp_err_t WebServer::getStatusHandler(httpd_req_t* req) { + cJSON* json = cJSON_CreateObject(); + cJSON_AddBoolToObject(json, "sdMounted", SdCard::instance().isMounted()); + cJSON_AddBoolToObject(json, "usbMscActive", SdCard::instance().isUsbMscActive()); + return sendJsonObject(req, json); +} diff --git a/main/webserver/WebServer.cpp b/main/webserver/WebServer.cpp new file mode 100644 index 0000000..80c1f9f --- /dev/null +++ b/main/webserver/WebServer.cpp @@ -0,0 +1,306 @@ +#include "WebServer.hpp" +#include "../SdCard.hpp" +#include "esp_log.h" +#include +#include +#include + +static const char* TAG = "WebServer"; + +WebServer& WebServer::instance() { + static WebServer instance; + return instance; +} + +WebServer::~WebServer() { + stop(); +} + +void WebServer::start() { + if (server_ != nullptr) { + ESP_LOGW(TAG, "Server already running"); + return; + } + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.uri_match_fn = httpd_uri_match_wildcard; + config.stack_size = 8192; + config.max_uri_handlers = 20; + + ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port); + + if (httpd_start(&server_, &config) != ESP_OK) { + ESP_LOGE(TAG, "Failed to start HTTP server"); + return; + } + + // Static file routes + httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = rootHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &root); + + httpd_uri_t webseite = { .uri = "/webseite/*", .method = HTTP_GET, .handler = staticFileHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &webseite); + + httpd_uri_t images = { .uri = "/images/*", .method = HTTP_GET, .handler = imagesHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &images); + + // Config routes + httpd_uri_t getConfig = { .uri = "/api/config", .method = HTTP_GET, .handler = getConfigHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &getConfig); + + httpd_uri_t postConfig = { .uri = "/api/config", .method = HTTP_POST, .handler = postConfigHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &postConfig); + + httpd_uri_t postSave = { .uri = "/api/save", .method = HTTP_POST, .handler = postSaveHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &postSave); + + httpd_uri_t postReset = { .uri = "/api/reset", .method = HTTP_POST, .handler = postResetHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &postReset); + + // KNX routes + httpd_uri_t getKnxAddresses = { .uri = "/api/knx/addresses", .method = HTTP_GET, .handler = getKnxAddressesHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &getKnxAddresses); + + // Status routes + httpd_uri_t postUsbMode = { .uri = "/api/usb-mode", .method = HTTP_POST, .handler = postUsbModeHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &postUsbMode); + + httpd_uri_t getStatus = { .uri = "/api/status", .method = HTTP_GET, .handler = getStatusHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &getStatus); + + // File manager routes + httpd_uri_t fileManager = { .uri = "/files", .method = HTTP_GET, .handler = fileManagerHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &fileManager); + + httpd_uri_t filesList = { .uri = "/api/files/list", .method = HTTP_GET, .handler = filesListHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &filesList); + + httpd_uri_t filesUpload = { .uri = "/api/files/upload", .method = HTTP_POST, .handler = filesUploadHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &filesUpload); + + httpd_uri_t filesDownload = { .uri = "/api/files/download", .method = HTTP_GET, .handler = filesDownloadHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &filesDownload); + + httpd_uri_t filesDelete = { .uri = "/api/files/delete", .method = HTTP_DELETE, .handler = filesDeleteHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &filesDelete); + + httpd_uri_t filesMkdir = { .uri = "/api/files/mkdir", .method = HTTP_POST, .handler = filesMkdirHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &filesMkdir); + + httpd_uri_t filesRead = { .uri = "/api/files/read", .method = HTTP_GET, .handler = filesReadHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &filesRead); + + httpd_uri_t filesWrite = { .uri = "/api/files/write", .method = HTTP_POST, .handler = filesWriteHandler, .user_ctx = nullptr }; + httpd_register_uri_handler(server_, &filesWrite); + + ESP_LOGI(TAG, "HTTP server started successfully"); +} + +void WebServer::stop() { + if (server_ != nullptr) { + httpd_stop(server_); + server_ = nullptr; + ESP_LOGI(TAG, "HTTP server stopped"); + } +} + +// ============================================================================ +// Shared Utility Functions +// ============================================================================ + +const char* WebServer::getContentType(const char* filepath) { + const char* ext = strrchr(filepath, '.'); + if (!ext) return "application/octet-stream"; + + if (strcasecmp(ext, ".html") == 0 || strcasecmp(ext, ".htm") == 0) return "text/html; charset=utf-8"; + if (strcasecmp(ext, ".css") == 0) return "text/css"; + if (strcasecmp(ext, ".js") == 0) return "application/javascript"; + if (strcasecmp(ext, ".json") == 0) return "application/json"; + if (strcasecmp(ext, ".png") == 0) return "image/png"; + if (strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0) return "image/jpeg"; + if (strcasecmp(ext, ".gif") == 0) return "image/gif"; + if (strcasecmp(ext, ".svg") == 0) return "image/svg+xml"; + if (strcasecmp(ext, ".ico") == 0) return "image/x-icon"; + + return "application/octet-stream"; +} + +// Case-insensitive path resolution for FAT filesystem +bool resolveCaseInsensitivePath(const char* requestedPath, char* resolvedPath, size_t maxLen) { + strncpy(resolvedPath, SdCard::MOUNT_POINT, maxLen); + resolvedPath[maxLen - 1] = '\0'; + + const char* relPath = requestedPath; + if (strncmp(requestedPath, SdCard::MOUNT_POINT, strlen(SdCard::MOUNT_POINT)) == 0) { + relPath = requestedPath + strlen(SdCard::MOUNT_POINT); + } + + if (!relPath || !*relPath || (relPath[0] == '/' && !relPath[1])) { + return true; + } + + char component[256]; + const char* start = relPath; + + while (*start) { + while (*start == '/') start++; + if (!*start) break; + + const char* end = start; + while (*end && *end != '/') end++; + + size_t compLen = end - start; + if (compLen >= sizeof(component)) compLen = sizeof(component) - 1; + memcpy(component, start, compLen); + component[compLen] = '\0'; + + DIR* dir = opendir(resolvedPath); + if (!dir) return false; + + bool found = false; + struct dirent* entry; + while ((entry = readdir(dir)) != nullptr) { + if (strcasecmp(entry->d_name, component) == 0) { + size_t curLen = strlen(resolvedPath); + snprintf(resolvedPath + curLen, maxLen - curLen, "/%s", entry->d_name); + found = true; + break; + } + } + closedir(dir); + + if (!found) return false; + start = end; + } + + return true; +} + +esp_err_t WebServer::sendFile(httpd_req_t* req, const char* filepath) { + char resolvedPath[384]; + const char* actualPath = filepath; + + struct stat st; + if (stat(filepath, &st) != 0) { + if (resolveCaseInsensitivePath(filepath, resolvedPath, sizeof(resolvedPath))) { + if (stat(resolvedPath, &st) == 0) { + actualPath = resolvedPath; + } + } + } + + if (stat(actualPath, &st) != 0) { + ESP_LOGE(TAG, "File not found: %s", filepath); + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found"); + return ESP_FAIL; + } + + FILE* f = fopen(actualPath, "r"); + if (!f) { + ESP_LOGE(TAG, "Failed to open file: %s", actualPath); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to open file"); + return ESP_FAIL; + } + + httpd_resp_set_type(req, getContentType(actualPath)); + + char* chunk = new char[1024]; + if (!chunk) { + fclose(f); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); + return ESP_FAIL; + } + + size_t read; + while ((read = fread(chunk, 1, 1024, f)) > 0) { + if (httpd_resp_send_chunk(req, chunk, read) != ESP_OK) { + delete[] chunk; + fclose(f); + return ESP_FAIL; + } + } + + delete[] chunk; + fclose(f); + httpd_resp_send_chunk(req, nullptr, 0); + return ESP_OK; +} + +// URL decode helper (in-place) +void urlDecode(char* str) { + char* dst = str; + char* src = str; + while (*src) { + if (*src == '%' && src[1] && src[2]) { + int hi = src[1]; + int lo = src[2]; + hi = (hi >= '0' && hi <= '9') ? hi - '0' : + (hi >= 'A' && hi <= 'F') ? hi - 'A' + 10 : + (hi >= 'a' && hi <= 'f') ? hi - 'a' + 10 : -1; + lo = (lo >= '0' && lo <= '9') ? lo - '0' : + (lo >= 'A' && lo <= 'F') ? lo - 'A' + 10 : + (lo >= 'a' && lo <= 'f') ? lo - 'a' + 10 : -1; + if (hi >= 0 && lo >= 0) { + *dst++ = (char)((hi << 4) | lo); + src += 3; + continue; + } + } else if (*src == '+') { + *dst++ = ' '; + src++; + continue; + } + *dst++ = *src++; + } + *dst = '\0'; +} + +bool WebServer::getQueryParam(httpd_req_t* req, const char* key, char* value, size_t maxLen) { + size_t queryLen = httpd_req_get_url_query_len(req); + if (queryLen == 0) return false; + + char* query = new char[queryLen + 1]; + if (!query) return false; + + if (httpd_req_get_url_query_str(req, query, queryLen + 1) != ESP_OK) { + delete[] query; + return false; + } + + esp_err_t ret = httpd_query_key_value(query, key, value, maxLen); + delete[] query; + + if (ret == ESP_OK) { + urlDecode(value); + return true; + } + return false; +} + +// JSON response helpers +esp_err_t WebServer::sendJsonObject(httpd_req_t* req, cJSON* json) { + char* str = cJSON_PrintUnformatted(json); + if (!str) { + cJSON_Delete(json); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON print failed"); + return ESP_FAIL; + } + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, str, strlen(str)); + free(str); + cJSON_Delete(json); + return ESP_OK; +} + +esp_err_t WebServer::sendJsonError(httpd_req_t* req, const char* error) { + cJSON* json = cJSON_CreateObject(); + cJSON_AddBoolToObject(json, "success", false); + cJSON_AddStringToObject(json, "error", error); + return sendJsonObject(req, json); +} + +esp_err_t WebServer::sendJsonSuccess(httpd_req_t* req) { + cJSON* json = cJSON_CreateObject(); + cJSON_AddBoolToObject(json, "success", true); + return sendJsonObject(req, json); +} diff --git a/main/webserver/WebServer.hpp b/main/webserver/WebServer.hpp new file mode 100644 index 0000000..0cc6a1c --- /dev/null +++ b/main/webserver/WebServer.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include "esp_http_server.h" +#include "cJSON.h" + +class WebServer { +public: + static WebServer& instance(); + + void start(); + void stop(); + bool isRunning() const { return server_ != nullptr; } + +private: + WebServer() = default; + ~WebServer(); + WebServer(const WebServer&) = delete; + WebServer& operator=(const WebServer&) = delete; + + httpd_handle_t server_ = nullptr; + + // Static file handlers (StaticFileHandlers.cpp) + static esp_err_t rootHandler(httpd_req_t* req); + static esp_err_t staticFileHandler(httpd_req_t* req); + static esp_err_t imagesHandler(httpd_req_t* req); + + // Config handlers (ConfigHandlers.cpp) + static esp_err_t getConfigHandler(httpd_req_t* req); + static esp_err_t postConfigHandler(httpd_req_t* req); + static esp_err_t postSaveHandler(httpd_req_t* req); + static esp_err_t postResetHandler(httpd_req_t* req); + + // KNX handlers (KnxHandlers.cpp) + static esp_err_t getKnxAddressesHandler(httpd_req_t* req); + + // Status handlers (StatusHandlers.cpp) + static esp_err_t postUsbModeHandler(httpd_req_t* req); + static esp_err_t getStatusHandler(httpd_req_t* req); + + // File manager handlers (FileManagerHandlers.cpp) + static esp_err_t fileManagerHandler(httpd_req_t* req); + static esp_err_t filesListHandler(httpd_req_t* req); + static esp_err_t filesUploadHandler(httpd_req_t* req); + static esp_err_t filesDownloadHandler(httpd_req_t* req); + static esp_err_t filesDeleteHandler(httpd_req_t* req); + static esp_err_t filesMkdirHandler(httpd_req_t* req); + static esp_err_t filesReadHandler(httpd_req_t* req); + static esp_err_t filesWriteHandler(httpd_req_t* req); + +public: + // Shared utility functions (used by handlers) + static const char* getContentType(const char* filepath); + static esp_err_t sendFile(httpd_req_t* req, const char* filepath); + static bool getQueryParam(httpd_req_t* req, const char* key, char* value, size_t maxLen); + + // JSON response helpers + static esp_err_t sendJsonObject(httpd_req_t* req, cJSON* json); + static esp_err_t sendJsonError(httpd_req_t* req, const char* error); + static esp_err_t sendJsonSuccess(httpd_req_t* req); +}; + +// Case-insensitive path resolution for FAT filesystem +bool resolveCaseInsensitivePath(const char* requestedPath, char* resolvedPath, size_t maxLen); + +// URL decode helper +void urlDecode(char* str);