From 8d1823d0b65e61f448c38feb06ad1650cf7c655a Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:43:13 +0100 Subject: [PATCH] First iteration that seems to work --- .gitignore | 83 + README.md | 167 ++ zephcore/CMakeLists.txt | 370 +++ zephcore/Kconfig | 424 +++ zephcore/adapters/ble/ZephyrBLE.cpp | 793 +++++ zephcore/adapters/ble/ZephyrBLE.h | 110 + zephcore/adapters/board/.gitkeep | 1 + zephcore/adapters/board/ZephyrBoard.cpp | 192 ++ zephcore/adapters/board/ZephyrBoard.h | 24 + zephcore/adapters/clock/.gitkeep | 1 + .../adapters/clock/ZephyrMillisecondClock.cpp | 15 + .../adapters/clock/ZephyrMillisecondClock.h | 17 + zephcore/adapters/clock/ZephyrRTCClock.cpp | 22 + zephcore/adapters/clock/ZephyrRTCClock.h | 21 + .../adapters/datastore/ZephyrDataStore.cpp | 739 +++++ zephcore/adapters/datastore/ZephyrDataStore.h | 86 + zephcore/adapters/gps/ZephyrGPSManager.cpp | 923 ++++++ zephcore/adapters/gps/ZephyrGPSManager.h | 118 + zephcore/adapters/radio/.gitkeep | 1 + zephcore/adapters/radio/LR1110Radio.cpp | 98 + zephcore/adapters/radio/LR1110Radio.h | 35 + zephcore/adapters/radio/LoRaRadioBase.cpp | 489 ++++ zephcore/adapters/radio/LoRaRadioBase.h | 174 ++ zephcore/adapters/radio/SX126xRadio.cpp | 237 ++ zephcore/adapters/radio/SX126xRadio.h | 42 + zephcore/adapters/radio/lr11xx/lr11xx_hal.h | 198 ++ .../adapters/radio/lr11xx/lr11xx_hal_zephyr.c | 344 +++ .../adapters/radio/lr11xx/lr11xx_hal_zephyr.h | 99 + zephcore/adapters/radio/lr11xx/lr11xx_radio.c | 1167 ++++++++ zephcore/adapters/radio/lr11xx/lr11xx_radio.h | 776 +++++ .../radio/lr11xx/lr11xx_radio_timings.c | 233 ++ .../radio/lr11xx/lr11xx_radio_timings.h | 95 + .../radio/lr11xx/lr11xx_radio_types.h | 573 ++++ .../adapters/radio/lr11xx/lr11xx_regmem.c | 314 ++ .../adapters/radio/lr11xx/lr11xx_regmem.h | 202 ++ .../adapters/radio/lr11xx/lr11xx_system.c | 659 +++++ .../adapters/radio/lr11xx/lr11xx_system.h | 585 ++++ .../radio/lr11xx/lr11xx_system_types.h | 330 +++ zephcore/adapters/radio/lr11xx/lr11xx_types.h | 76 + zephcore/adapters/radio/radio_common.h | 68 + zephcore/adapters/rng/RNG.cpp | 20 + zephcore/adapters/rng/ZephyrRNG.cpp | 20 + zephcore/adapters/rng/ZephyrRNG.h | 17 + zephcore/adapters/sensors/SimpleLPP.h | 257 ++ .../adapters/sensors/ZephyrEnvSensors.cpp | 384 +++ zephcore/adapters/sensors/ZephyrEnvSensors.h | 70 + .../adapters/sensors/ZephyrSensorManager.h | 27 + zephcore/adapters/usb/ZephyrCompanionUSB.cpp | 251 ++ zephcore/adapters/usb/ZephyrCompanionUSB.h | 46 + zephcore/adapters/usb/ZephyrRepeaterUSB.cpp | 178 ++ zephcore/adapters/usb/ZephyrRepeaterUSB.h | 32 + zephcore/app/CompanionMesh.cpp | 2573 +++++++++++++++++ zephcore/app/CompanionMesh.h | 319 ++ zephcore/app/RepeaterDataStore.cpp | 263 ++ zephcore/app/RepeaterDataStore.h | 47 + zephcore/app/RepeaterMesh.cpp | 1168 ++++++++ zephcore/app/RepeaterMesh.h | 189 ++ zephcore/boards/common/esp32_common.conf | 21 + zephcore/boards/common/filesystem.dtsi | 27 + zephcore/boards/common/logging.conf | 21 + zephcore/boards/common/mg24_common.conf | 26 + zephcore/boards/common/nrf52_common.conf | 35 + zephcore/boards/common/nrf54l_common.conf | 30 + zephcore/boards/common/packet_logging.conf | 34 + zephcore/boards/common/prod.conf | 17 + zephcore/boards/common/qspi-ext.dtsi | 25 + zephcore/boards/common/repeater.conf | 7 + zephcore/boards/common/sensors-i2c.dtsi | 105 + zephcore/boards/common/zephcore_common.conf | 157 + zephcore/boards/esp32/xiao_esp32c3/board.conf | 22 + .../boards/esp32/xiao_esp32c3/board.overlay | 79 + zephcore/boards/esp32/xiao_esp32c6/board.conf | 22 + .../boards/esp32/xiao_esp32c6/board.overlay | 79 + zephcore/boards/example_board/README.md | 95 + zephcore/boards/example_board/board.conf | 98 + zephcore/boards/example_board/board.overlay | 720 +++++ zephcore/boards/mg24/xiao_mg24/board.conf | 12 + zephcore/boards/mg24/xiao_mg24/board.overlay | 99 + .../ikoka_nano_30dbm/Kconfig.ikoka_nano_30dbm | 5 + .../nrf52840/ikoka_nano_30dbm/board.conf | 25 + .../nrf52840/ikoka_nano_30dbm/board.yml | 6 + .../ikoka_nano_30dbm-pinctrl.dtsi | 87 + .../ikoka_nano_30dbm/ikoka_nano_30dbm.dts | 223 ++ .../ikoka_nano_30dbm_defconfig | 30 + zephcore/boards/nrf52840/rak4631/board.conf | 27 + .../boards/nrf52840/rak4631/board.overlay | 155 + .../boards/nrf52840/t1000_e/Kconfig.t1000_e | 6 + zephcore/boards/nrf52840/t1000_e/board.cmake | 9 + zephcore/boards/nrf52840/t1000_e/board.conf | 23 + .../boards/nrf52840/t1000_e/board.overlay | 55 + zephcore/boards/nrf52840/t1000_e/board.yml | 10 + .../t1000_e/t1000_e_nrf52840-pinctrl.dtsi | 80 + .../nrf52840/t1000_e/t1000_e_nrf52840.dts | 325 +++ .../nrf52840/t1000_e/t1000_e_nrf52840.yaml | 20 + .../t1000_e/t1000_e_nrf52840_defconfig | 21 + .../wio_tracker_l1/Kconfig.wio_tracker_l1 | 6 + .../nrf52840/wio_tracker_l1/board.cmake | 14 + .../boards/nrf52840/wio_tracker_l1/board.conf | 26 + .../nrf52840/wio_tracker_l1/board.overlay | 28 + .../boards/nrf52840/wio_tracker_l1/board.yml | 10 + .../wio_tracker_l1_nrf52840-pinctrl.dtsi | 123 + .../wio_tracker_l1_nrf52840.dts | 390 +++ .../wio_tracker_l1_nrf52840.yaml | 20 + .../wio_tracker_l1_nrf52840_defconfig | 21 + .../boards/nrf54l/xiao_nrf54l15/board.conf | 11 + .../boards/nrf54l/xiao_nrf54l15/board.overlay | 98 + .../input/zephcore,input-multi-tap.yaml | 57 + zephcore/dts/bindings/vendor-prefixes.txt | 1 + zephcore/helpers/AdvertDataHelpers.cpp | 71 + zephcore/helpers/AdvertDataHelpers.h | 77 + zephcore/helpers/BaseChatMesh.cpp | 945 ++++++ zephcore/helpers/BaseChatMesh.h | 201 ++ zephcore/helpers/ChannelDetails.h | 14 + zephcore/helpers/ClientACL.cpp | 164 ++ zephcore/helpers/ClientACL.h | 80 + zephcore/helpers/CommonCLI.cpp | 700 +++++ zephcore/helpers/CommonCLI.h | 93 + zephcore/helpers/ContactInfo.h | 34 + zephcore/helpers/IdentityStore.h | 12 + zephcore/helpers/NodePrefs.h | 111 + zephcore/helpers/RateLimiter.h | 37 + zephcore/helpers/RegionMap.cpp | 310 ++ zephcore/helpers/RegionMap.h | 66 + zephcore/helpers/StatsFormatHelper.h | 61 + zephcore/helpers/TransportKeyStore.cpp | 110 + zephcore/helpers/TransportKeyStore.h | 38 + zephcore/helpers/TxtDataHelpers.h | 48 + zephcore/helpers/oled_power.c | 45 + zephcore/helpers/oled_power.h | 26 + zephcore/helpers/ui/buzzer.c | 468 +++ zephcore/helpers/ui/buzzer.h | 93 + zephcore/helpers/ui/cfb_font_0608.c | 925 ++++++ zephcore/helpers/ui/display.c | 340 +++ zephcore/helpers/ui/display.h | 144 + zephcore/helpers/ui/doom_game.c | 1208 ++++++++ zephcore/helpers/ui/doom_game.h | 69 + zephcore/helpers/ui/input_multi_tap.c | 170 ++ zephcore/helpers/ui/ui_mesh_actions.cpp | 291 ++ zephcore/helpers/ui/ui_mesh_actions.h | 67 + zephcore/helpers/ui/ui_pages.c | 694 +++++ zephcore/helpers/ui/ui_pages.h | 162 ++ zephcore/helpers/ui/ui_task.c | 1005 +++++++ zephcore/helpers/ui/ui_task.h | 165 ++ zephcore/include/mesh/Board.h | 41 + zephcore/include/mesh/Clock.h | 17 + zephcore/include/mesh/Dispatcher.h | 141 + zephcore/include/mesh/Identity.h | 57 + zephcore/include/mesh/LoRaConfig.h | 21 + zephcore/include/mesh/Mesh.h | 86 + zephcore/include/mesh/MeshCore.h | 34 + zephcore/include/mesh/Packet.h | 70 + zephcore/include/mesh/RNG.h | 20 + zephcore/include/mesh/RTC.h | 32 + zephcore/include/mesh/Radio.h | 38 + zephcore/include/mesh/RadioIncludes.h | 32 + zephcore/include/mesh/SimpleMeshTables.h | 77 + .../include/mesh/StaticPoolPacketManager.h | 26 + zephcore/include/mesh/Utils.h | 28 + zephcore/lib/.gitkeep | 1 + zephcore/lib/ed25519/CMakeLists.txt | 18 + zephcore/lib/ed25519/add_scalar.c | 69 + zephcore/lib/ed25519/ed_25519.h | 41 + zephcore/lib/ed25519/fe.c | 1491 ++++++++++ zephcore/lib/ed25519/fe.h | 41 + zephcore/lib/ed25519/fixedint.h | 72 + zephcore/lib/ed25519/ge.c | 467 +++ zephcore/lib/ed25519/ge.h | 74 + zephcore/lib/ed25519/key_exchange.c | 79 + zephcore/lib/ed25519/keypair.c | 23 + zephcore/lib/ed25519/license.txt | 16 + zephcore/lib/ed25519/precomp_data.h | 1391 +++++++++ zephcore/lib/ed25519/sc.c | 809 ++++++ zephcore/lib/ed25519/sc.h | 12 + zephcore/lib/ed25519/seed.c | 40 + zephcore/lib/ed25519/sha512.c | 275 ++ zephcore/lib/ed25519/sha512.h | 21 + zephcore/lib/ed25519/sign.c | 31 + zephcore/lib/ed25519/verify.c | 77 + .../lib/loramac-node/src/radio/sx126x/radio.c | 1413 +++++++++ .../drivers/gnss/Kconfig.luatos_air530z | 43 + .../zephyr/drivers/gnss/gnss_luatos_air530z.c | 328 +++ .../zephyr/drivers/lora/CMakeLists.txt | 12 + zephcore/patches/zephyr/drivers/lora/Kconfig | 68 + .../drivers/lora/loramac-node/sx12xx_common.c | 462 +++ .../zephyr/drivers/lora/lr11xx/CMakeLists.txt | 24 + .../zephyr/drivers/lora/lr11xx/Kconfig | 12 + .../zephyr/drivers/lora/lr11xx/lr11xx_lora.c | 873 ++++++ .../zephyr/drivers/lora/lr11xx/lr11xx_lora.h | 80 + .../dts/bindings/lora/semtech,lr1110.yaml | 86 + .../zephyr/scripts/west_commands/blobs.py | 346 +++ zephcore/prj.conf | 30 + zephcore/src/Dispatcher.cpp | 426 +++ zephcore/src/Identity.cpp | 130 + zephcore/src/Mesh.cpp | 668 +++++ zephcore/src/Packet.cpp | 75 + zephcore/src/StaticPoolPacketManager.cpp | 145 + zephcore/src/Utils.cpp | 226 ++ zephcore/src/main_companion.cpp | 643 ++++ zephcore/src/main_repeater.cpp | 375 +++ zephcore/tools/formatter/CMakeLists.txt | 60 + .../formatter/boards/rak4631/board.overlay | 61 + .../boards/wio_tracker_l1/board.conf | 3 + zephcore/tools/formatter/prj.conf | 42 + zephcore/tools/formatter/src/main.c | 128 + zephcore/west.yml | 13 + 205 files changed, 41989 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 zephcore/CMakeLists.txt create mode 100644 zephcore/Kconfig create mode 100644 zephcore/adapters/ble/ZephyrBLE.cpp create mode 100644 zephcore/adapters/ble/ZephyrBLE.h create mode 100644 zephcore/adapters/board/.gitkeep create mode 100644 zephcore/adapters/board/ZephyrBoard.cpp create mode 100644 zephcore/adapters/board/ZephyrBoard.h create mode 100644 zephcore/adapters/clock/.gitkeep create mode 100644 zephcore/adapters/clock/ZephyrMillisecondClock.cpp create mode 100644 zephcore/adapters/clock/ZephyrMillisecondClock.h create mode 100644 zephcore/adapters/clock/ZephyrRTCClock.cpp create mode 100644 zephcore/adapters/clock/ZephyrRTCClock.h create mode 100644 zephcore/adapters/datastore/ZephyrDataStore.cpp create mode 100644 zephcore/adapters/datastore/ZephyrDataStore.h create mode 100644 zephcore/adapters/gps/ZephyrGPSManager.cpp create mode 100644 zephcore/adapters/gps/ZephyrGPSManager.h create mode 100644 zephcore/adapters/radio/.gitkeep create mode 100644 zephcore/adapters/radio/LR1110Radio.cpp create mode 100644 zephcore/adapters/radio/LR1110Radio.h create mode 100644 zephcore/adapters/radio/LoRaRadioBase.cpp create mode 100644 zephcore/adapters/radio/LoRaRadioBase.h create mode 100644 zephcore/adapters/radio/SX126xRadio.cpp create mode 100644 zephcore/adapters/radio/SX126xRadio.h create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_hal.h create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.h create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_radio.c create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_radio.h create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_radio_timings.c create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_radio_timings.h create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_radio_types.h create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_regmem.c create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_regmem.h create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_system.c create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_system.h create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_system_types.h create mode 100644 zephcore/adapters/radio/lr11xx/lr11xx_types.h create mode 100644 zephcore/adapters/radio/radio_common.h create mode 100644 zephcore/adapters/rng/RNG.cpp create mode 100644 zephcore/adapters/rng/ZephyrRNG.cpp create mode 100644 zephcore/adapters/rng/ZephyrRNG.h create mode 100644 zephcore/adapters/sensors/SimpleLPP.h create mode 100644 zephcore/adapters/sensors/ZephyrEnvSensors.cpp create mode 100644 zephcore/adapters/sensors/ZephyrEnvSensors.h create mode 100644 zephcore/adapters/sensors/ZephyrSensorManager.h create mode 100644 zephcore/adapters/usb/ZephyrCompanionUSB.cpp create mode 100644 zephcore/adapters/usb/ZephyrCompanionUSB.h create mode 100644 zephcore/adapters/usb/ZephyrRepeaterUSB.cpp create mode 100644 zephcore/adapters/usb/ZephyrRepeaterUSB.h create mode 100644 zephcore/app/CompanionMesh.cpp create mode 100644 zephcore/app/CompanionMesh.h create mode 100644 zephcore/app/RepeaterDataStore.cpp create mode 100644 zephcore/app/RepeaterDataStore.h create mode 100644 zephcore/app/RepeaterMesh.cpp create mode 100644 zephcore/app/RepeaterMesh.h create mode 100644 zephcore/boards/common/esp32_common.conf create mode 100644 zephcore/boards/common/filesystem.dtsi create mode 100644 zephcore/boards/common/logging.conf create mode 100644 zephcore/boards/common/mg24_common.conf create mode 100644 zephcore/boards/common/nrf52_common.conf create mode 100644 zephcore/boards/common/nrf54l_common.conf create mode 100644 zephcore/boards/common/packet_logging.conf create mode 100644 zephcore/boards/common/prod.conf create mode 100644 zephcore/boards/common/qspi-ext.dtsi create mode 100644 zephcore/boards/common/repeater.conf create mode 100644 zephcore/boards/common/sensors-i2c.dtsi create mode 100644 zephcore/boards/common/zephcore_common.conf create mode 100644 zephcore/boards/esp32/xiao_esp32c3/board.conf create mode 100644 zephcore/boards/esp32/xiao_esp32c3/board.overlay create mode 100644 zephcore/boards/esp32/xiao_esp32c6/board.conf create mode 100644 zephcore/boards/esp32/xiao_esp32c6/board.overlay create mode 100644 zephcore/boards/example_board/README.md create mode 100644 zephcore/boards/example_board/board.conf create mode 100644 zephcore/boards/example_board/board.overlay create mode 100644 zephcore/boards/mg24/xiao_mg24/board.conf create mode 100644 zephcore/boards/mg24/xiao_mg24/board.overlay create mode 100644 zephcore/boards/nrf52840/ikoka_nano_30dbm/Kconfig.ikoka_nano_30dbm create mode 100644 zephcore/boards/nrf52840/ikoka_nano_30dbm/board.conf create mode 100644 zephcore/boards/nrf52840/ikoka_nano_30dbm/board.yml create mode 100644 zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm-pinctrl.dtsi create mode 100644 zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts create mode 100644 zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm_defconfig create mode 100644 zephcore/boards/nrf52840/rak4631/board.conf create mode 100644 zephcore/boards/nrf52840/rak4631/board.overlay create mode 100644 zephcore/boards/nrf52840/t1000_e/Kconfig.t1000_e create mode 100644 zephcore/boards/nrf52840/t1000_e/board.cmake create mode 100644 zephcore/boards/nrf52840/t1000_e/board.conf create mode 100644 zephcore/boards/nrf52840/t1000_e/board.overlay create mode 100644 zephcore/boards/nrf52840/t1000_e/board.yml create mode 100644 zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840-pinctrl.dtsi create mode 100644 zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts create mode 100644 zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.yaml create mode 100644 zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840_defconfig create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1/Kconfig.wio_tracker_l1 create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1/board.cmake create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1/board.conf create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1/board.overlay create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1/board.yml create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840-pinctrl.dtsi create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.yaml create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840_defconfig create mode 100644 zephcore/boards/nrf54l/xiao_nrf54l15/board.conf create mode 100644 zephcore/boards/nrf54l/xiao_nrf54l15/board.overlay create mode 100644 zephcore/dts/bindings/input/zephcore,input-multi-tap.yaml create mode 100644 zephcore/dts/bindings/vendor-prefixes.txt create mode 100644 zephcore/helpers/AdvertDataHelpers.cpp create mode 100644 zephcore/helpers/AdvertDataHelpers.h create mode 100644 zephcore/helpers/BaseChatMesh.cpp create mode 100644 zephcore/helpers/BaseChatMesh.h create mode 100644 zephcore/helpers/ChannelDetails.h create mode 100644 zephcore/helpers/ClientACL.cpp create mode 100644 zephcore/helpers/ClientACL.h create mode 100644 zephcore/helpers/CommonCLI.cpp create mode 100644 zephcore/helpers/CommonCLI.h create mode 100644 zephcore/helpers/ContactInfo.h create mode 100644 zephcore/helpers/IdentityStore.h create mode 100644 zephcore/helpers/NodePrefs.h create mode 100644 zephcore/helpers/RateLimiter.h create mode 100644 zephcore/helpers/RegionMap.cpp create mode 100644 zephcore/helpers/RegionMap.h create mode 100644 zephcore/helpers/StatsFormatHelper.h create mode 100644 zephcore/helpers/TransportKeyStore.cpp create mode 100644 zephcore/helpers/TransportKeyStore.h create mode 100644 zephcore/helpers/TxtDataHelpers.h create mode 100644 zephcore/helpers/oled_power.c create mode 100644 zephcore/helpers/oled_power.h create mode 100644 zephcore/helpers/ui/buzzer.c create mode 100644 zephcore/helpers/ui/buzzer.h create mode 100644 zephcore/helpers/ui/cfb_font_0608.c create mode 100644 zephcore/helpers/ui/display.c create mode 100644 zephcore/helpers/ui/display.h create mode 100644 zephcore/helpers/ui/doom_game.c create mode 100644 zephcore/helpers/ui/doom_game.h create mode 100644 zephcore/helpers/ui/input_multi_tap.c create mode 100644 zephcore/helpers/ui/ui_mesh_actions.cpp create mode 100644 zephcore/helpers/ui/ui_mesh_actions.h create mode 100644 zephcore/helpers/ui/ui_pages.c create mode 100644 zephcore/helpers/ui/ui_pages.h create mode 100644 zephcore/helpers/ui/ui_task.c create mode 100644 zephcore/helpers/ui/ui_task.h create mode 100644 zephcore/include/mesh/Board.h create mode 100644 zephcore/include/mesh/Clock.h create mode 100644 zephcore/include/mesh/Dispatcher.h create mode 100644 zephcore/include/mesh/Identity.h create mode 100644 zephcore/include/mesh/LoRaConfig.h create mode 100644 zephcore/include/mesh/Mesh.h create mode 100644 zephcore/include/mesh/MeshCore.h create mode 100644 zephcore/include/mesh/Packet.h create mode 100644 zephcore/include/mesh/RNG.h create mode 100644 zephcore/include/mesh/RTC.h create mode 100644 zephcore/include/mesh/Radio.h create mode 100644 zephcore/include/mesh/RadioIncludes.h create mode 100644 zephcore/include/mesh/SimpleMeshTables.h create mode 100644 zephcore/include/mesh/StaticPoolPacketManager.h create mode 100644 zephcore/include/mesh/Utils.h create mode 100644 zephcore/lib/.gitkeep create mode 100644 zephcore/lib/ed25519/CMakeLists.txt create mode 100644 zephcore/lib/ed25519/add_scalar.c create mode 100644 zephcore/lib/ed25519/ed_25519.h create mode 100644 zephcore/lib/ed25519/fe.c create mode 100644 zephcore/lib/ed25519/fe.h create mode 100644 zephcore/lib/ed25519/fixedint.h create mode 100644 zephcore/lib/ed25519/ge.c create mode 100644 zephcore/lib/ed25519/ge.h create mode 100644 zephcore/lib/ed25519/key_exchange.c create mode 100644 zephcore/lib/ed25519/keypair.c create mode 100644 zephcore/lib/ed25519/license.txt create mode 100644 zephcore/lib/ed25519/precomp_data.h create mode 100644 zephcore/lib/ed25519/sc.c create mode 100644 zephcore/lib/ed25519/sc.h create mode 100644 zephcore/lib/ed25519/seed.c create mode 100644 zephcore/lib/ed25519/sha512.c create mode 100644 zephcore/lib/ed25519/sha512.h create mode 100644 zephcore/lib/ed25519/sign.c create mode 100644 zephcore/lib/ed25519/verify.c create mode 100644 zephcore/patches/modules/lib/loramac-node/src/radio/sx126x/radio.c create mode 100644 zephcore/patches/zephyr/drivers/gnss/Kconfig.luatos_air530z create mode 100644 zephcore/patches/zephyr/drivers/gnss/gnss_luatos_air530z.c create mode 100644 zephcore/patches/zephyr/drivers/lora/CMakeLists.txt create mode 100644 zephcore/patches/zephyr/drivers/lora/Kconfig create mode 100644 zephcore/patches/zephyr/drivers/lora/loramac-node/sx12xx_common.c create mode 100644 zephcore/patches/zephyr/drivers/lora/lr11xx/CMakeLists.txt create mode 100644 zephcore/patches/zephyr/drivers/lora/lr11xx/Kconfig create mode 100644 zephcore/patches/zephyr/drivers/lora/lr11xx/lr11xx_lora.c create mode 100644 zephcore/patches/zephyr/drivers/lora/lr11xx/lr11xx_lora.h create mode 100644 zephcore/patches/zephyr/dts/bindings/lora/semtech,lr1110.yaml create mode 100644 zephcore/patches/zephyr/scripts/west_commands/blobs.py create mode 100644 zephcore/prj.conf create mode 100644 zephcore/src/Dispatcher.cpp create mode 100644 zephcore/src/Identity.cpp create mode 100644 zephcore/src/Mesh.cpp create mode 100644 zephcore/src/Packet.cpp create mode 100644 zephcore/src/StaticPoolPacketManager.cpp create mode 100644 zephcore/src/Utils.cpp create mode 100644 zephcore/src/main_companion.cpp create mode 100644 zephcore/src/main_repeater.cpp create mode 100644 zephcore/tools/formatter/CMakeLists.txt create mode 100644 zephcore/tools/formatter/boards/rak4631/board.overlay create mode 100644 zephcore/tools/formatter/boards/wio_tracker_l1/board.conf create mode 100644 zephcore/tools/formatter/prj.conf create mode 100644 zephcore/tools/formatter/src/main.c create mode 100644 zephcore/west.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ba9592 --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# Build output (west build creates these) +/build*/ +build/ +build-*/ +**/build/ +**/build-*/ +/twister-out*/ + +# West workspace (recreated by west init/update) +/.west/ + +# Zephyr SDK and toolchain (large, downloaded separately) +/bootloader/ +/modules/ +/tools/ +/zephyr/ + +# Python +.venv/ +__pycache__/ +*.pyc + +# Editors / IDE +.vscode/ +.idea/ +*.swp +*~ +*.bak + +# Debug artifacts +*.log +jlink_*.txt +rtt_*.log + +# UF2 / binary artifacts +*.uf2 +*.hex +*.bin +*.elf +*.map +*.obj +*.o +*.a + +# OS junk +.DS_Store +Thumbs.db +desktop.ini + +# Claude Code project data +.claude/ + +# RTT output files (missed by *.log pattern) +rtt_*.txt + +# JLink scripts and outputs +*.jlink + +# Documentation that was moved/deleted +/docs/ + +# Temporary/scratch files +*.tmp +*.orig + +# CMake/Ninja build system +CMakeCache.txt +CMakeFiles/ +.ninja_* +cmake_install.cmake +Makefile +compile_commands.json + +# Memory dumps +*.dmp +core + +# Utility/test source files +meshcore/src/eraser_main.cpp +zephcore/AUDIT_FINDINGS.md + +# Side projects (not part of main firmware) +/doom/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4bce1d --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# ZephCore — MeshCore for Zephyr RTOS + +A port of [MeshCore](https://github.com/meshcore-dev/MeshCore/) LoRa mesh firmware from Arduino to [Zephyr RTOS](https://zephyrproject.org/). Aiming for full protocol compatibility with the original Arduino firmware and the MeshCore mobile apps. + +DISCLAIMER: +I did this to be a PoC for my Wio Tracker and nRF repeater. +The porting flow was this: +- Copy the meshcore companion source and hit it with a hammer named Claude until the FW works on the node +- if the FW is stable, hit it until it works with the official app +- if both goal is reached, hit the repeater fw and make it work exactly like the arduino counterpart +- Add T1000 and make LR1110 work +- try to fix a thousand bugs that the hammering did +- Add new boards that doesn't even work on arduino (that was not a choice....) +- Try to make it universal so new boards/radios/whatever can be plugged in +- Try to clean up the code and hunt down anything that I've missed <-- YOU ARE HERE NOW +- Revise everything once again, try to understand more than just a quarter of it +- Cleanup-debug--port upstream changes-anything that pops in my mind +- ??? +- Profit + +Wio Tracker L1 companion and Ikoka Nano 30dB repeater is working according to my tests, I can't vouch for anything else yet. +It's compatible with the current bootloaders I use, so I can roll back to arduino version easily and I have DFU. +Repeater's "touch 1200" and other methods to enter DFU aren't tested yet. + + + +And now, the sacred AI texts: + + +## Why Zephyr? + +The Arduino version uses a `loop()`. This port replaces that with Zephyr's event-driven primitives (`k_event_wait`, `k_poll`, `k_msgq`), so the CPU sleeps in WFI (Wait For Interrupt) between events. + +Other benefits: + +- **Proper driver model** -- LoRa, GNSS, display, sensors, and BLE all use Zephyr subsystem drivers rather than Arduino libraries +- **Hierarchical build configuration** -- board-specific settings compose cleanly via Kconfig and devicetree overlays +- **DFU support** -- generates Arduino compatible zip packages for OTA updates and UF2 binaries for drag-and-drop flashing +- **Back and forth compatible** -- Adapted to softdevice and adafruit's bootloader, so no bootloader re-flashing required. + +## Supported Boards + +| Board | MCU | Radio | Extras | +|-------|-----|-------|--------| +| **Wio Tracker L1** | nRF52840 | SX1262 | GPS (L76K), OLED display, joystick, buzzer, QSPI flash | +| **Seeed T1000-E** | nRF52840 | LR1110 | GPS (Mediatek AG3335), LEDs, button | +| **Ikoka Nano 30dBm** | XIAO nRF52840 | SX1262 (E22-900M30S, 30dBm PA) | RGB LEDs | + +## Device Roles + +**Companion** (default) + +**Repeater** + +## Building + +Prerequisites: [Zephyr SDK](https://docs.zephyrproject.org/latest/develop/getting_started/index.html) and `west` installed. + +```bash +# Initialize workspace (first time only) +cd %cloned folder% +west init -l zephcore +west update + +# Companion (with logging) +west build -b wio_tracker_l1 zephcore --pristine + +# Companion (production, no logging) +west build -b wio_tracker_l1 zephcore --pristine -- \ + -DEXTRA_CONF_FILE="boards/common/prod.conf" + +# Repeater (with logging) +west build -b rak4631/nrf52840 zephcore --pristine -- \ + -DEXTRA_CONF_FILE="boards/common/repeater.conf" + +# Repeater (production) +west build -b rak4631/nrf52840 zephcore --pristine -- \ + -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/prod.conf" + +# Repeater with packet logging (clean RAW/RX/TX lines only, no debug spam) +west build -b rak4631/nrf52840 zephcore --pristine -- \ + -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/packet_logging.conf" + +# Formatter (with serial logging) +west build -b wio_tracker_l1 zephcore/tools/formatter --pristine + +# Companion (BLE debug logging) +west build -b rak4631/nrf52840 zephcore --pristine -- -DCONFIG_ZEPHCORE_BLE_LOG_LEVEL_DBG=y +``` + +Output binaries are in `build/zephyr/` -- `.hex`, `.uf2`, and DFU `.zip` as applicable. + +## Architecture Overview + +``` +Mobile App <--BLE (NUS)--> [ Companion ] <--LoRa--> Mesh Network + | + k_event_wait() + / | \ + LORA_RX LORA_TX_DONE BLE_RX +``` + +All code paths are event-driven. No polling loops, no busy waits. + +- **LoRa RX**: Zephyr driver callback signals the mesh event loop +- **LoRa TX**: A dedicated 512-byte thread blocks on `k_poll()`, restarts RX immediately on completion, then notifies the mesh loop +- **BLE**: NUS write handler enqueues to `k_msgq` and signals the mesh loop; TX uses `bt_gatt_notify_cb()` chaining +- **Main loop**: `k_event_wait()` blocks until work arrives + +### Key Differences from Arduino + +| | Arduino | Zephyr | +|---|---------|--------| +| Main loop | 50ms polling | `k_event_wait()` (blocks until event) | +| LoRa TX | Blocking `transmit()` | Async thread with `k_poll()` | +| BLE queues | Ring buffer + mutex | `k_msgq` (lock-free, ISR-safe) | +| Drivers | Arduino libraries | Zephyr subsystem drivers | +| Configuration | `#define` constants | Kconfig + devicetree | +| Threading | Single-threaded `loop()` | Multi-threaded with explicit sync | +| Power | Always-on loop | WFI sleep between events | + +## Power Saving + +- **LoRa RX duty cycle**: CAD-based receive windowing reduces LoRa RX current from ~10-15mA to ~3-5mA (configurable via `CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE`) +- **USB disabled in production**: Saves ~2-5mA and 62KB flash when logging is off +- **GPIO-gated GPS**: Powered on only during fix acquisition + +## Configuration + +Key Kconfig options (set in board configs or via `-D` flags): + +| Option | Default | Description | +|--------|---------|-------------| +| `CONFIG_ZEPHCORE_ROLE_COMPANION` | y | BLE companion mode | +| `CONFIG_ZEPHCORE_ROLE_REPEATER` | n | USB CLI repeater mode | +| `CONFIG_ZEPHCORE_RADIO_NATIVE` | y | SX1261/62/68/(7x?) radio | +| `CONFIG_ZEPHCORE_RADIO_LR1110` | n | LR1110/LR1120 radio | +| `CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE` | y | RX power saving | +| `CONFIG_ZEPHCORE_MAX_CONTACTS` | 350 | Contact storage slots | +| `CONFIG_ZEPHCORE_MAX_CHANNELS` | 40 | Channel slots | +| `CONFIG_ZEPHCORE_BLE_PASSKEY` | 123456 | BLE pairing PIN | +| `CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC` | 300 | GPS fix interval | + +## Project Structure + +``` +zephcore/ + src/ Main entry points and core mesh protocol + app/ Companion and repeater role implementations + adapters/ + radio/ LoRa radio drivers (SX126x, LR1110) + board/ GPIO, LED, power management + datastore/ LittleFS filesystem wrapper + sensors/ I2C sensor auto-detection + clock/ Millisecond and RTC clocks + helpers/ + ui/ Display, buzzer, button input + boards/ + nrf52840/ Per-board devicetree overlays and configs + common/ Shared Kconfig fragments and devicetree includes + lib/ ED25519 crypto library + patches/ Auto-applied patches to the Zephyr tree +``` + +## License + +Same license as the upstream MeshCore project. \ No newline at end of file diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt new file mode 100644 index 0000000..15edc21 --- /dev/null +++ b/zephcore/CMakeLists.txt @@ -0,0 +1,370 @@ +# SPDX-License-Identifier: Apache-2.0 +# ZephCore Zephyr - full port + +cmake_minimum_required(VERSION 3.20.0) + +# ============================================================================ +# Zephyr Patch Auto-Apply +# ============================================================================ +# Patches stored in zephcore/patches/zephyr/ are copied to Zephyr source tree +# at configure time. This survives `west update` - patches are reapplied on +# next build. +# +# Current patches: +# zephyr/: +# - drivers/lora/loramac-node/sx12xx_common.c: bandwidth enum→index mapping +# - drivers/lora/lr11xx/*: LR11xx Zephyr LoRa driver (for future upstream PR) +# - drivers/lora/CMakeLists.txt: adds lr11xx subdirectory +# - drivers/lora/Kconfig: adds lr11xx Kconfig +# - dts/bindings/lora/semtech,lr1110.yaml: LR1110 DTS binding +# - drivers/gnss/gnss_luatos_air530z.c: EASY ephemeris prediction (PMTK869) +# - drivers/gnss/Kconfig.luatos_air530z: CONFIG_GNSS_LUATOS_AIR530Z_EASY +# modules/: +# - lib/loramac-node/src/radio/sx126x/radio.c: restore full 10-element +# Bandwidths[] array + fix LDRO for sub-125kHz bandwidths +# +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr) + message(STATUS "Applying ZephCore patches to Zephyr...") + file(GLOB_RECURSE ZEPHCORE_PATCH_FILES + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr + ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr/*) + foreach(REL_PATH ${ZEPHCORE_PATCH_FILES}) + set(SRC_FILE ${CMAKE_CURRENT_SOURCE_DIR}/patches/zephyr/${REL_PATH}) + # ZEPHYR_BASE is set by find_package(Zephyr), but we're before that. + # Use the parent directory structure: zephcore/../zephyr + get_filename_component(ZEPHYR_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../zephyr ABSOLUTE) + set(DST_FILE ${ZEPHYR_DIR}/${REL_PATH}) + message(STATUS " Patching: ${REL_PATH}") + configure_file(${SRC_FILE} ${DST_FILE} COPYONLY) + endforeach() +endif() + +# Patch modules (loramac-node, etc.) - same mechanism, different target dir +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/patches/modules) + message(STATUS "Applying ZephCore patches to modules...") + file(GLOB_RECURSE ZEPHCORE_MODULE_PATCH_FILES + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/patches/modules + ${CMAKE_CURRENT_SOURCE_DIR}/patches/modules/*) + foreach(REL_PATH ${ZEPHCORE_MODULE_PATCH_FILES}) + set(SRC_FILE ${CMAKE_CURRENT_SOURCE_DIR}/patches/modules/${REL_PATH}) + get_filename_component(MODULES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../modules ABSOLUTE) + set(DST_FILE ${MODULES_DIR}/${REL_PATH}) + message(STATUS " Patching: ${REL_PATH}") + configure_file(${SRC_FILE} ${DST_FILE} COPYONLY) + endforeach() +endif() + +# Add custom boards directory (for Wio Tracker L1, etc.) +# Zephyr expects: /boards/// +# Our structure: zephcore/boards/// +# So we add zephcore/ as BOARD_ROOT, and Zephyr finds boards/nrf52840/wio_tracker_l1/ +list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) + +# ========== Board Configuration Hierarchy ========== +# Include order: prj.conf → zephcore_common.conf → _common.conf → /board.conf +# +# Structure: +# boards/common/zephcore_common.conf - ALL boards (crypto, sensors, LoRa) +# boards/common/nrf52_common.conf - nRF52 boards (BLE, flash, GNSS) +# boards//board.conf - Board-specific (pins, features) +# boards//prod.conf - Production overrides (optional) +# +# To add a new board: +# 1. Create boards//board.conf with pins and unique features +# 2. Create boards//board.overlay with hardware definitions +# 3. Build: west build -b zephcore --pristine + +# Build common config file list based on detected platform +set(ZEPHCORE_COMMON_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/zephcore_common.conf") + +# Detect platform from board name and add platform-specific common config +# For boards with qualifiers like "wio_tracker_l1/nrf52840", check both the full BOARD +# string and the BOARD_QUALIFIERS which contains just the qualifier (e.g., "nrf52840") +if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e") + set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") +elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf52.*") + set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") +elseif(BOARD MATCHES ".*esp32.*") + set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_common.conf") +elseif(BOARD MATCHES ".*nrf54l.*") + set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf54l_common.conf") +elseif(BOARD MATCHES ".*mg24.*" OR BOARD MATCHES ".*efr32.*") + set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/mg24_common.conf") +else() + set(ZEPHCORE_PLATFORM_CONF "") +endif() + +# Check for board-specific config in new folder structure +# Priority: boards//board.conf > boards/_.conf (legacy) +# +# For custom boards (e.g., wio_tracker_l1/nrf52840), BOARD contains the base name +# For Zephyr boards with qualifiers (e.g., rak4631/nrf52840), BOARD contains base/qualifier +# We extract just the base board name for folder lookup +string(REPLACE "/" ";" BOARD_PARTS ${BOARD}) +list(GET BOARD_PARTS 0 BOARD_BASE) # Get base name without qualifier + +# Search for board.conf in possible locations +set(ZEPHCORE_BOARD_CONF "") +file(GLOB_RECURSE BOARD_CONF_CANDIDATES "${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/board.conf") +if(BOARD_CONF_CANDIDATES) + list(GET BOARD_CONF_CANDIDATES 0 ZEPHCORE_BOARD_CONF) +endif() +# Fallback: direct path without vendor subdirectory +if(NOT ZEPHCORE_BOARD_CONF AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.conf") + set(ZEPHCORE_BOARD_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.conf") +endif() + +# Build the EXTRA_CONF_FILE list (append to any user-provided extras) +set(ZEPHCORE_CONF_FILES "") +if(EXISTS ${ZEPHCORE_COMMON_CONF}) + list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_COMMON_CONF}) +endif() +if(ZEPHCORE_PLATFORM_CONF AND EXISTS ${ZEPHCORE_PLATFORM_CONF}) + list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_PLATFORM_CONF}) +endif() +if(ZEPHCORE_BOARD_CONF AND EXISTS ${ZEPHCORE_BOARD_CONF}) + list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_BOARD_CONF}) +endif() + +# Include logging tuning only for debug builds (when prod.conf is NOT active). +# This avoids Kconfig warnings from log settings that depend on CONFIG_LOG. +set(ZEPHCORE_LOGGING_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/logging.conf") +if(EXTRA_CONF_FILE MATCHES "prod\\.conf") + message(STATUS " Production build — skipping logging.conf") +else() + if(EXISTS ${ZEPHCORE_LOGGING_CONF}) + list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_LOGGING_CONF}) + endif() +endif() + +# Append to EXTRA_CONF_FILE (preserves any user-specified extras) +# Note: repeater.conf is passed via EXTRA_CONF_FILE on the command line when building repeater role +if(ZEPHCORE_CONF_FILES) + if(EXTRA_CONF_FILE) + set(EXTRA_CONF_FILE "${EXTRA_CONF_FILE};${ZEPHCORE_CONF_FILES}" CACHE STRING "" FORCE) + else() + set(EXTRA_CONF_FILE "${ZEPHCORE_CONF_FILES}" CACHE STRING "" FORCE) + endif() +endif() + +# ========== DTC Overlay Hierarchy ========== +# Search for board.overlay in possible locations (same as board.conf) +set(ZEPHCORE_BOARD_OVERLAY "") +file(GLOB_RECURSE BOARD_OVERLAY_CANDIDATES "${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/board.overlay") +if(BOARD_OVERLAY_CANDIDATES) + list(GET BOARD_OVERLAY_CANDIDATES 0 ZEPHCORE_BOARD_OVERLAY) +endif() +# Fallback: direct path without vendor subdirectory +if(NOT ZEPHCORE_BOARD_OVERLAY AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay") + set(ZEPHCORE_BOARD_OVERLAY "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay") +endif() + +# Append to EXTRA_DTC_OVERLAY_FILE (preserves any user-specified extras) +if(ZEPHCORE_BOARD_OVERLAY AND EXISTS ${ZEPHCORE_BOARD_OVERLAY}) + if(EXTRA_DTC_OVERLAY_FILE) + set(EXTRA_DTC_OVERLAY_FILE "${EXTRA_DTC_OVERLAY_FILE};${ZEPHCORE_BOARD_OVERLAY}" CACHE STRING "" FORCE) + else() + set(EXTRA_DTC_OVERLAY_FILE "${ZEPHCORE_BOARD_OVERLAY}" CACHE STRING "" FORCE) + endif() +endif() + +# Debug: Show what configs are being used +message(STATUS "ZephCore config hierarchy:") +message(STATUS " Common: ${ZEPHCORE_COMMON_CONF}") +message(STATUS " Platform: ${ZEPHCORE_PLATFORM_CONF}") +message(STATUS " Board conf: ${ZEPHCORE_BOARD_CONF}") +message(STATUS " Board overlay: ${ZEPHCORE_BOARD_OVERLAY}") +message(STATUS " EXTRA_CONF_FILE: ${EXTRA_CONF_FILE}") +message(STATUS " EXTRA_DTC_OVERLAY_FILE: ${EXTRA_DTC_OVERLAY_FILE}") + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(zephcore) + +# Dynamic build date - always current, even without --pristine +string(TIMESTAMP ZEPHCORE_BUILD_DATE "%Y %b %d" UTC) +add_definitions(-DFIRMWARE_BUILD_DATE="${ZEPHCORE_BUILD_DATE}") + +add_subdirectory(lib/ed25519) +target_link_libraries(app PRIVATE ed25519) + +target_include_directories(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/helpers + ${CMAKE_CURRENT_SOURCE_DIR}/app + ${CMAKE_CURRENT_SOURCE_DIR}/lib/ed25519 + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/board + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/clock + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/datastore + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/radio + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/rng + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/gps + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/sensors + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/ble +) + +# ========== Core Sources (both roles) ========== +target_sources(app PRIVATE + src/Dispatcher.cpp + src/Identity.cpp + src/Mesh.cpp + src/Packet.cpp + src/StaticPoolPacketManager.cpp + src/Utils.cpp + adapters/board/ZephyrBoard.cpp + adapters/radio/LoRaRadioBase.cpp + adapters/clock/ZephyrMillisecondClock.cpp + adapters/clock/ZephyrRTCClock.cpp + adapters/rng/RNG.cpp + adapters/rng/ZephyrRNG.cpp + adapters/gps/ZephyrGPSManager.cpp + adapters/sensors/ZephyrEnvSensors.cpp + helpers/AdvertDataHelpers.cpp + helpers/oled_power.c +) + +# ========== Radio Driver Selection ========== +if(CONFIG_ZEPHCORE_RADIO_LR1110) + message(STATUS "ZephCore Radio: LR1110 (Zephyr LoRa driver)") + # Driver + Semtech SDK compiled by Zephyr via patched drivers/lora/lr11xx/ + # (patches/zephyr/ auto-applied at configure time — see patch block above) + # Only the ZephCore adapter is compiled here. + target_sources(app PRIVATE + adapters/radio/LR1110Radio.cpp + ) + target_include_directories(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/radio/lr11xx + ) + # Driver header (lr11xx_lora.h) is in the Zephyr tree after patching + get_filename_component(ZEPHYR_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../zephyr ABSOLUTE) + target_include_directories(app PRIVATE + ${ZEPHYR_DIR}/drivers/lora/lr11xx + ) +else() + # Default: SX126x via Zephyr LoRa driver + message(STATUS "ZephCore Radio: SX126x (Zephyr LoRa driver)") + target_sources(app PRIVATE + adapters/radio/SX126xRadio.cpp + ) +endif() + +# ========== Role-Specific Sources ========== +if(CONFIG_ZEPHCORE_ROLE_REPEATER) + message(STATUS "ZephCore Role: REPEATER") + target_sources(app PRIVATE + src/main_repeater.cpp + app/RepeaterMesh.cpp + app/RepeaterDataStore.cpp + helpers/ClientACL.cpp + helpers/RegionMap.cpp + helpers/TransportKeyStore.cpp + helpers/CommonCLI.cpp + ) + # USB CDC with 1200 baud touch detection (when not using auto-init) + if(NOT CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) + target_sources(app PRIVATE + adapters/usb/ZephyrRepeaterUSB.cpp + ) + endif() + target_include_directories(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/usb + ) + target_compile_definitions(app PRIVATE ZEPHCORE_REPEATER=1) +else() + message(STATUS "ZephCore Role: COMPANION") + target_sources(app PRIVATE + src/main_companion.cpp + adapters/ble/ZephyrBLE.cpp + adapters/datastore/ZephyrDataStore.cpp + helpers/BaseChatMesh.cpp + helpers/TransportKeyStore.cpp + helpers/ui/ui_mesh_actions.cpp + app/CompanionMesh.cpp + ) + # USB CDC companion transport (only in debug builds with logging) + if(CONFIG_LOG) + target_sources(app PRIVATE + adapters/usb/ZephyrCompanionUSB.cpp + ) + endif() + target_include_directories(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/adapters/usb + ) + target_compile_definitions(app PRIVATE ZEPHCORE_COMPANION=1) +endif() + +# ========== UI Helpers (conditional on Kconfig) ========== +if(CONFIG_ZEPHCORE_UI_MULTI_TAP) + target_sources(app PRIVATE + helpers/ui/input_multi_tap.c + ) +endif() + +if(CONFIG_ZEPHCORE_UI_BUZZER) + target_sources(app PRIVATE + helpers/ui/buzzer.c + ) +endif() + +if(CONFIG_ZEPHCORE_UI_DISPLAY) + target_sources(app PRIVATE + helpers/ui/display.c + helpers/ui/ui_pages.c + helpers/ui/cfb_font_0608.c + ) +endif() + +if(CONFIG_ZEPHCORE_UI_BUTTONS OR CONFIG_ZEPHCORE_UI_BUZZER OR CONFIG_ZEPHCORE_UI_DISPLAY) + target_sources(app PRIVATE + helpers/ui/ui_task.c + ) + target_include_directories(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/helpers/ui + ) +endif() + +if(CONFIG_ZEPHCORE_EASTER_EGG_DOOM) + target_sources(app PRIVATE + helpers/ui/doom_game.c + ) +endif() + +# ========== Post-build: ESP32 flash instructions ========== +if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common") + # ESP32 simple boot: zephyr.bin is a self-contained image at 0x0. + # It includes embedded bootloader code — no separate bootloader needed. + # NVS (0x3B0000) and LittleFS (0x3B2000) are NOT touched by flashing. + message(STATUS "") + message(STATUS "ESP32 Flash Commands:") + message(STATUS " Normal update (preserves NVS + user data):") + message(STATUS " esptool --port COMx --baud 921600 write_flash 0x0 build_dir/zephyr/zephyr.bin") + message(STATUS " Full flash (after erase_flash):") + message(STATUS " esptool --port COMx --baud 921600 erase_flash") + message(STATUS " esptool --port COMx --baud 921600 write_flash 0x0 build_dir/zephyr/zephyr.bin") + message(STATUS " Or use: west flash --build-dir build_dir") + message(STATUS "") +endif() + +# ========== Post-build: Generate DFU zip for OTA updates (nRF52 only) ========== +# This runs after ALL build steps (linking, objcopy) via a custom target +# Only applicable on nRF52 boards with Adafruit bootloader +find_program(ADAFRUIT_NRFUTIL adafruit-nrfutil) +if(ADAFRUIT_NRFUTIL AND ZEPHCORE_PLATFORM_CONF MATCHES "nrf52_common") + set(DFU_ZIP "${CMAKE_BINARY_DIR}/zephyr/zephyr.zip") + set(DFU_HEX "${CMAKE_BINARY_DIR}/zephyr/zephyr.hex") + add_custom_target(dfu_zip ALL + COMMAND ${ADAFRUIT_NRFUTIL} dfu genpkg + --dev-type 0x0052 + --sd-req ${CONFIG_ZEPHCORE_SD_FWID} + --application ${DFU_HEX} + ${DFU_ZIP} + DEPENDS ${DFU_HEX} + COMMENT "Generating DFU package: zephyr.zip (sd-req=${CONFIG_ZEPHCORE_SD_FWID})" + VERBATIM + ) + # Ensure dfu_zip runs after Zephyr's final target generates the hex + add_dependencies(dfu_zip zephyr_final) + message(STATUS "DFU zip generation: ENABLED (adafruit-nrfutil found)") +else() + message(STATUS "DFU zip generation: DISABLED (adafruit-nrfutil not found)") +endif() diff --git a/zephcore/Kconfig b/zephcore/Kconfig new file mode 100644 index 0000000..bfa4cea --- /dev/null +++ b/zephcore/Kconfig @@ -0,0 +1,424 @@ +# SPDX-License-Identifier: Apache-2.0 + +source "$(ZEPHYR_BASE)/Kconfig.zephyr" + +module = ZEPHCORE_MAIN +module-str = zephcore_main +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +module = ZEPHCORE_BLE +module-str = zephcore_ble +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +module = ZEPHCORE_LORA +module-str = zephcore_lora +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +module = ZEPHCORE_BOARD +module-str = zephcore_board +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +module = ZEPHCORE_DATASTORE +module-str = zephcore_datastore +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +module = ZEPHCORE_GPS +module-str = zephcore_gps +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +module = ZEPHCORE_SENSORS +module-str = zephcore_sensors +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +module = ZEPHCORE_USB +module-str = zephcore_usb +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +module = ZEPHCORE_UI_ACTIONS +module-str = zephcore_ui_actions +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +menu "ZephCore" + +menu "Device Role" + +choice ZEPHCORE_ROLE + prompt "Device Role" + default ZEPHCORE_ROLE_COMPANION + help + Select the device role. Companion devices connect to mobile apps via BLE. + Repeaters forward packets and are configured via USB serial CLI. + +config ZEPHCORE_ROLE_COMPANION + bool "Companion Device" + help + BLE companion device that connects to ZephCore mobile app. + Includes BLE stack, contact management, offline queue, channels. + +config ZEPHCORE_ROLE_REPEATER + bool "Repeater" + help + LoRa mesh repeater with USB serial CLI interface. + No BLE stack. Has ACL authentication, region filtering, + neighbor tracking, and full CLI command set. + +endchoice + +if ZEPHCORE_ROLE_REPEATER + +config ZEPHCORE_MAX_CLIENTS + int "Maximum ACL clients" + default 32 + help + Maximum clients in the Access Control List. + Each client stores pubkey, permissions, and shared secret. + +config ZEPHCORE_MAX_NEIGHBOURS + int "Maximum tracked neighbors" + default 16 + help + Maximum number of neighboring repeaters to track. + Set to 0 to disable neighbor tracking. + +config ZEPHCORE_MAX_REGION_ENTRIES + int "Maximum region entries" + default 32 + help + Maximum entries in the region map for flood filtering. + Regions control which transport codes are allowed to flood. + +config ZEPHCORE_ADMIN_PASSWORD + string "Default admin password" + default "password" + help + Default password for admin access to the repeater. + Should be changed on first configuration! + +config ZEPHCORE_GUEST_PASSWORD + string "Default guest password" + default "" + help + Default password for guest access. Empty string disables guest access. + +endif # ZEPHCORE_ROLE_REPEATER + +endmenu # Device Role + +if ZEPHCORE_ROLE_COMPANION + +config ZEPHCORE_MAX_CONTACTS + int "Maximum number of contacts" + default 350 + help + Maximum number of contacts that can be stored. + +config ZEPHCORE_MAX_CHANNELS + int "Maximum number of channels" + default 40 + help + Maximum number of group channels that can be stored. + +config ZEPHCORE_OFFLINE_QUEUE_SIZE + int "Offline message queue size" + default 16 + help + Size of the offline message queue for incoming messages. + +config ZEPHCORE_ACK_TABLE_SIZE + int "ACK tracking table size" + default 8 + help + Number of pending ACKs that can be tracked. + +config ZEPHCORE_MAX_CONNECTIONS + int "Maximum server connections" + default 16 + help + Maximum number of simultaneous room server connections with keep-alive. + +config ZEPHCORE_ADVERT_PATH_TABLE_SIZE + int "Advert path table size" + default 16 + help + Size of recently-heard advert path table. + +menu "BLE Configuration" + +config ZEPHCORE_BLE_PASSKEY + int "BLE pairing passkey (6 digits)" + default 123456 + range 0 999999 + help + Fixed 6-digit passkey for BLE MITM pairing. + +config ZEPHCORE_BLE_QUEUE_SIZE + int "BLE TX/RX message queue depth" + default 12 + range 4 64 + help + Number of frames that can be queued for BLE TX/RX. + Larger = better buffering for bursty traffic, more RAM. + +config ZEPHCORE_BLE_CONN_MIN_INTERVAL + int "BLE minimum connection interval (units: 1.25ms)" + default 12 + range 6 3200 + help + Minimum BLE connection interval in 1.25ms units. + 12 = 15ms. Matches Arduino default. Lower = more responsive, higher power. + +config ZEPHCORE_BLE_CONN_MAX_INTERVAL + int "BLE maximum connection interval (units: 1.25ms)" + default 36 + range 6 3200 + help + Maximum BLE connection interval in 1.25ms units. + 36 = 45ms. Matches Arduino default. Higher = lower power when idle. + +config ZEPHCORE_BLE_CONN_LATENCY + int "BLE connection latency (skip events)" + default 4 + range 0 500 + help + Skip up to N connection events when idle to save power. + 4 = skip 4 events (effective interval up to 375ms idle). + +config ZEPHCORE_BLE_CONN_TIMEOUT + int "BLE supervision timeout (units: 10ms)" + default 500 + range 10 3200 + help + Supervision timeout in 10ms units. + 500 = 5000ms. Connection drops if no data for this duration. + +config ZEPHCORE_BLE_ADV_FAST_INTERVAL + int "BLE fast advertising interval (units: 0.625ms)" + default 32 + range 20 1600 + help + Fast advertising interval in 0.625ms units. + 32 = 20ms (Apple-recommended for quick discovery). + +config ZEPHCORE_BLE_ADV_SLOW_INTERVAL + int "BLE slow advertising interval (units: 0.625ms)" + default 874 + range 400 3200 + help + Slow advertising interval in 0.625ms units. + 874 = 546.25ms (Apple-compliant, battery-friendly). + +config ZEPHCORE_BLE_ADV_FAST_TIMEOUT + int "Duration of fast advertising mode (seconds)" + default 30 + range 10 300 + help + How long to advertise at the fast rate before switching to slow. + +endmenu # BLE Configuration + +endif # ZEPHCORE_ROLE_COMPANION + +config ZEPHCORE_HOUSEKEEPING_INTERVAL_MS + int "Periodic housekeeping interval (ms)" + default 5000 + range 1000 60000 + help + Wake interval for noise floor calibration, battery reads, and UI refresh. + Longer = lower power, less responsive monitoring. + +menu "Radio Configuration" + +choice ZEPHCORE_RADIO_TYPE + prompt "LoRa Radio Type" + default ZEPHCORE_RADIO_NATIVE + help + Select the LoRa radio driver. + +config ZEPHCORE_RADIO_NATIVE + bool "Zephyr native LoRa driver (SX126x, SX127x, LLCC68, STM32WL)" + help + Uses Zephyr's built-in LoRa driver. Supports all radios that Zephyr + has device tree bindings for: + SX126x: SX1261, SX1262, SX1268 + SX127x: SX1272, SX1276, SX1278 + LLCC68: Semtech LLCC68 + STM32WL: STM32WL SUBGHZ radio (SX126x-based) + Used by: RAK4631, Wio Tracker L1, all XIAO + Wio-SX1262 boards + +config ZEPHCORE_RADIO_LR1110 + bool "LR1110/LR1120/LR1121 (custom driver)" + select LORA + help + Semtech LR11xx via custom ZephCore driver (not upstream Zephyr). + Uses SPI mutex for thread safety + BUSY stuck recovery. + TCXO, RF switch, and PA config are in the device tree. + For any board with an LR1110, LR1120, or LR1121 radio. + +endchoice + +endmenu # Radio Configuration + +menu "Board Configuration" + +config ZEPHCORE_BOARD_NAME + string "Board name for identification" + default "Zephyr Device" + help + Human-readable board name returned by the 'board' CLI command + and reported in device info. Should match Arduino variant names + for consistency (e.g., "RAK 4631", "Wio Tracker L1", "T1000-E"). + +config ZEPHCORE_SD_FWID + hex "SoftDevice firmware ID for DFU packages" + default 0x00B6 + depends on SOC_SERIES_NRF52 + help + SoftDevice firmware ID used by adafruit-nrfutil when generating + DFU zip packages (--sd-req parameter). Must match the SoftDevice + version flashed with the Adafruit bootloader on the target device. + Only applicable to nRF52 boards with Adafruit bootloader. + + Common values: + 0x00B6 - S140 v6.1.1 (RAK4631, Wio Tracker, T1000-E) + 0x0123 - S140 v7.3.0 (XIAO nRF52840, Ikoka Nano) + +config ZEPHCORE_VBAT_MV_MULTIPLIER + int "Battery voltage ADC multiplier (fallback)" + default 7200 + help + Fallback multiplier if vbat-mv-multiplier is not defined in + devicetree zephyr,user node. + + Prefer defining in devicetree alongside ADC channel: + zephyr,user { + io-channels = <&adc 7>; + vbat-mv-multiplier = <7200>; + }; + + Formula: mv = (raw * multiplier) / 4096 + +endmenu + +menu "Debug & Logging" + +config ZEPHCORE_PACKET_LOGGING + bool "Enable mesh packet logging" + default n + depends on LOG + help + Log all RX and TX mesh packets in Arduino-compatible format. + Output format matches Arduino MESH_PACKET_LOGGING exactly: + "HH:MM:SS - D/M/YYYY U: RX, len=N (type=N, route=D/F, payload_len=N) SNR=N RSSI=N score=N time=N hash=XXXX [XX -> XX]" + +config ZEPHCORE_PACKET_LOGGING_ONLY + bool "Quiet mode - only packet logs, no debug spam" + default n + depends on ZEPHCORE_PACKET_LOGGING + help + When enabled, suppresses all other log messages and only outputs + packet logging (RAW, RX, TX lines). Uses printk() to bypass + log level filtering. + +endmenu + +menu "LoRa Power Saving" + +config ZEPHCORE_LORA_RX_DUTY_CYCLE + bool "Enable LoRa RX duty cycle power saving" + default n + help + Enable RX duty cycling for power saving on battery-powered devices. + Uses RadioLib's algorithm to calculate optimal timing based on SF/BW. + + The key insight is that LoRa preamble symbols are all identical, + so CAD/RX can detect preamble even if we wake mid-stream. The + algorithm ensures we always catch at least 8 symbols regardless + of when we wake within the preamble window. + + Power savings: ~60-70% reduction in RX current (10-15mA to 3-5mA). + + Note: Still experimental. Disable if packets are being missed. + +endmenu + +menu "GPS Configuration" + +config ZEPHCORE_GPS_POLL_INTERVAL_SEC + int "GPS poll interval in seconds" + default 300 + range 10 86400 + help + How often to wake GPS and acquire a fix. + Default 300 seconds (5 minutes). + +config ZEPHCORE_GPS_FIX_TIMEOUT_SEC + int "GPS fix acquisition timeout in seconds" + default 30 + range 10 300 + help + Maximum time to wait for GPS fix before giving up. + GPS will be powered off after this timeout. + +endmenu + +menu "UI Configuration" + +config ZEPHCORE_UI_BUTTONS + bool "Enable button input" + default y + depends on INPUT + help + Enable button/joystick input with long-press, double-click, + and triple-click detection using Zephyr's native input subsystem. + +config ZEPHCORE_UI_MULTI_TAP + bool "Enable multi-tap input filter" + default y + depends on ZEPHCORE_UI_BUTTONS + help + Enable the multi-tap input filter that counts rapid taps + within a configurable window and emits different key codes + based on tap count. Supports 1-4 tap levels via devicetree. + Single-tap has ~400ms latency (waits for window to expire). + +config ZEPHCORE_UI_BUZZER + bool "Enable buzzer" + default y + depends on PWM + help + Enable PWM buzzer with RTTTL melody support. + Requires a PWM-capable pin connected to a buzzer. + +config ZEPHCORE_UI_DISPLAY + bool "Enable display (OLED, LCD, e-ink)" + default y + depends on DISPLAY + depends on CHARACTER_FRAMEBUFFER + help + Enable page-based UI on any Zephyr-supported display. + Auto-detects display via "zephyr,display" chosen node or + legacy nodelabels (sh1106, ssd1306). Resolution and font + are queried at runtime — no hardcoded dimensions. + Supports OLED (SSD1306, SH1106), LCD, e-ink, etc. + +config ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS + int "Display auto-off timeout (ms)" + default 15000 + depends on ZEPHCORE_UI_DISPLAY + help + Time in milliseconds before the display turns off + after the last user interaction. Set to 0 to disable. + +config ZEPHCORE_EASTER_EGG_DOOM + bool "Enable Doom raycaster easter egg" + default n + depends on ZEPHCORE_UI_DISPLAY && ZEPHCORE_UI_BUTTONS + help + Hidden easter egg: Wolf3D-style raycaster on the OLED. + Activate with double-click + single-click joystick center + on the first page. Double-click to exit. + Adds ~5KB flash and ~1.7KB RAM when enabled. + +endmenu + +endmenu diff --git a/zephcore/adapters/ble/ZephyrBLE.cpp b/zephcore/adapters/ble/ZephyrBLE.cpp new file mode 100644 index 0000000..a1c670b --- /dev/null +++ b/zephcore/adapters/ble/ZephyrBLE.cpp @@ -0,0 +1,793 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore BLE Adapter — NUS service, advertising, security, TX/RX + * + * Extracted from main_companion.cpp for independent BLE debug logging. + */ + +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_ble, CONFIG_ZEPHCORE_BLE_LOG_LEVEL); + +#include +#include +#include +#include +#include +#include + +#include "ZephyrBLE.h" + +/* MAX_FRAME_SIZE from CompanionMesh.h — keep in sync */ +#ifndef MAX_FRAME_SIZE +#define MAX_FRAME_SIZE 172 +#endif + +/* ========== Constants ========== */ + +#define DEVICE_NAME_MAX 24 +#define FRAME_QUEUE_SIZE CONFIG_ZEPHCORE_BLE_QUEUE_SIZE +#define BLE_TX_POWER 4 +#define BLE_TX_RETRY_MS 20 + +/* BLE connection parameters */ +#define BLE_DEFAULT_MIN_INTERVAL CONFIG_ZEPHCORE_BLE_CONN_MIN_INTERVAL +#define BLE_DEFAULT_MAX_INTERVAL CONFIG_ZEPHCORE_BLE_CONN_MAX_INTERVAL +#define BLE_DEFAULT_LATENCY CONFIG_ZEPHCORE_BLE_CONN_LATENCY +#define BLE_DEFAULT_TIMEOUT CONFIG_ZEPHCORE_BLE_CONN_TIMEOUT + +/* TX timeout watchdog - reset ble_tx_in_progress if callback never fires */ +#define BLE_TX_TIMEOUT_MS 2000 + +/* Advertising intervals (Apple Accessory Design Guidelines) */ +#define BT_ADV_INTERVAL_FAST CONFIG_ZEPHCORE_BLE_ADV_FAST_INTERVAL +#define BT_ADV_INTERVAL_SLOW CONFIG_ZEPHCORE_BLE_ADV_SLOW_INTERVAL +#define BT_ADV_FAST_TIMEOUT_SEC CONFIG_ZEPHCORE_BLE_ADV_FAST_TIMEOUT + +/* ========== Frame type for queues ========== */ + +struct frame { + uint16_t len; + uint8_t buf[MAX_FRAME_SIZE]; +}; + +/* ========== Static state ========== */ + +/* Callbacks to main */ +static const struct ble_callbacks *ble_cbs; + +/* Advertising data */ +static char device_name[DEVICE_NAME_MAX]; +static const uint8_t ad_flags = BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR; +static const int8_t ad_tx_power = BLE_TX_POWER; +static const uint8_t nus_uuid[] = { BT_UUID_NUS_SRV_VAL }; +static struct bt_data ad[3]; +static struct bt_data sd[1]; +static size_t ad_len; +static size_t sd_len; + +/* Queues — ISR-safe, no mutex needed */ +K_MSGQ_DEFINE(ble_send_queue, sizeof(struct frame), FRAME_QUEUE_SIZE, 4); +K_MSGQ_DEFINE(ble_recv_queue, sizeof(struct frame), FRAME_QUEUE_SIZE, 4); + +/* TX retry buffer - used when BLE returns -ENOMEM/-EAGAIN */ +static struct frame tx_retry_frame; +static bool tx_retry_pending = false; + +/* Connection state */ +static struct bt_conn *current_conn; +static bool nus_notif_enabled; +static bool ble_tx_ready = false; +static bool ble_tx_in_progress = false; +static int64_t ble_tx_start_time = 0; + +/* Active interface tracking */ +static enum zephcore_iface active_iface = ZEPHCORE_IFACE_NONE; + +/* Advertising state */ +static bool adv_switching = false; +static bool adv_is_slow = false; + +/* Runtime BLE passkey */ +static uint32_t ble_passkey = CONFIG_ZEPHCORE_BLE_PASSKEY; + +/* ========== Forward declarations ========== */ + +static void ble_tx_complete_cb(struct bt_conn *conn, void *user_data); +static int secure_nus_send(struct bt_conn *conn, const void *data, uint16_t len); +static void secure_nus_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value); +static ssize_t secure_nus_rx_write(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags); +static void start_adv(void); +static void kick_tx_drain(void); + +/* ========== GATT Service ========== */ + +/* + * Secure NUS service with MITM-authenticated permissions. + * Unlike the default NUS service, this requires PIN pairing before + * any NUS communication is allowed (matches Arduino's SECMODE_ENC_WITH_MITM). + */ +BT_GATT_SERVICE_DEFINE(secure_nus_svc, + BT_GATT_PRIMARY_SERVICE(BT_UUID_NUS_SERVICE), + BT_GATT_CHARACTERISTIC(BT_UUID_NUS_TX_CHAR, + BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_NONE, + NULL, NULL, NULL), + BT_GATT_CCC(secure_nus_ccc_changed, + BT_GATT_PERM_READ_AUTHEN | BT_GATT_PERM_WRITE_AUTHEN), + BT_GATT_CHARACTERISTIC(BT_UUID_NUS_RX_CHAR, + BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP, + BT_GATT_PERM_WRITE_AUTHEN, + NULL, secure_nus_rx_write, NULL), +); + +static sys_slist_t secure_nus_cbs_list = SYS_SLIST_STATIC_INIT(&secure_nus_cbs_list); +STRUCT_SECTION_ITERABLE(bt_nus_inst, secure_nus) = { + .svc = &secure_nus_svc, + .cbs = &secure_nus_cbs_list, +}; + +/* ========== Work items ========== */ + +static void tx_drain_work_fn(struct k_work *work); +static void adv_slow_work_fn(struct k_work *work); + +K_WORK_DELAYABLE_DEFINE(tx_drain_work, tx_drain_work_fn); +K_WORK_DELAYABLE_DEFINE(adv_slow_work, adv_slow_work_fn); + +/* ========== TX completion callback ========== */ + +/* + * TX completion callback - chains to next frame (event-driven like Arduino) + * This is called by bt_gatt_notify_cb when the notification is sent over the air. + */ +static void ble_tx_complete_cb(struct bt_conn *conn, void *user_data) +{ + ARG_UNUSED(conn); + ARG_UNUSED(user_data); + + ble_tx_in_progress = false; + + /* Chain to next frame immediately via work queue */ + k_work_schedule(&tx_drain_work, K_NO_WAIT); +} + +/* Helper function to send via our secure NUS TX characteristic with callback */ +static int secure_nus_send(struct bt_conn *conn, const void *data, uint16_t len) +{ + struct bt_gatt_notify_params params = { + .attr = &secure_nus_svc.attrs[1], /* TX characteristic */ + .data = data, + .len = len, + .func = ble_tx_complete_cb, + .user_data = NULL, + }; + + return bt_gatt_notify_cb(conn, ¶ms); +} + +/* ========== Device name and advertising data ========== */ + +static void build_device_name_and_adv(const char *name_from_prefs) +{ + if (name_from_prefs && name_from_prefs[0]) { + size_t name_len = strnlen(name_from_prefs, sizeof(device_name) - 1); + memcpy(device_name, name_from_prefs, name_len); + device_name[name_len] = '\0'; + + /* Apple BLE Accessory Design Guidelines: device name must not + * contain ':' or ';' characters. Replace with '-'. + */ + for (size_t i = 0; device_name[i]; i++) { + if (device_name[i] == ':' || device_name[i] == ';') { + device_name[i] = '-'; + } + } + } else { + /* Fallback - should never happen since prefs.node_name has default */ + snprintf(device_name, sizeof(device_name), "MeshCore"); + } + + bt_set_name(device_name); + + ad[0].type = BT_DATA_FLAGS; + ad[0].data_len = 1; + ad[0].data = &ad_flags; + ad[1].type = BT_DATA_TX_POWER; + ad[1].data_len = 1; + ad[1].data = (const uint8_t *)&ad_tx_power; + ad[2].type = BT_DATA_UUID128_ALL; + ad[2].data_len = sizeof(nus_uuid); + ad[2].data = nus_uuid; + ad_len = 3; + + sd[0].type = BT_DATA_NAME_COMPLETE; + sd[0].data_len = (uint8_t)strlen(device_name); + sd[0].data = (const uint8_t *)device_name; + sd_len = 1; + + LOG_DBG("%s", device_name); +} + +/* ========== Connection callbacks ========== */ + +static void connected(struct bt_conn *conn, uint8_t err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + if (err) { + LOG_WRN("connection failed: %s err 0x%02x", addr, err); + return; + } + LOG_DBG("%s", addr); + current_conn = bt_conn_ref(conn); + + /* Cancel slow advertising work - we're connected now */ + k_work_cancel_delayable(&adv_slow_work); + adv_is_slow = false; + + /* Request max data length (251 bytes) - must be before MTU exchange per BLE spec. + * Apple Accessory Design Guidelines: DLE before MTU handshake for best performance. + */ +#if defined(CONFIG_BT_USER_DATA_LEN_UPDATE) + { + struct bt_conn_le_data_len_param data_len_param = { + .tx_max_len = BT_GAP_DATA_LEN_MAX, + .tx_max_time = BT_GAP_DATA_TIME_MAX, + }; + int dle_err = bt_conn_le_data_len_update(conn, &data_len_param); + if (dle_err) { + LOG_WRN("Failed to request data length update: %d", dle_err); + } else { + LOG_INF("Requested max data length (251 bytes)"); + } + } +#endif + + /* Proactively request authenticated encryption (MITM). + * - Bonded device: re-encrypts with stored keys (instant, no user interaction) + * - New device: triggers passkey pairing dialog on the central + * + * L3 = authenticated pairing with MITM protection. iOS/Android will + * negotiate Secure Connections (LESC) automatically when supported. + * L4 (SC-only) is NOT used because Windows never shows the passkey + * dialog for SC pairing — it only works with Legacy passkey entry. + * + * This is needed because some BLE stacks (Windows) don't automatically + * initiate pairing when they get "Insufficient Authentication" from a + * protected characteristic. By requesting security here, all platforms + * (iOS, Android, Windows) get the pairing prompt immediately. + * + * security_changed() callback handles the rest once encryption is up. */ + int sec_err = bt_conn_set_security(conn, BT_SECURITY_L3); + if (sec_err && sec_err != -EALREADY) { + LOG_WRN("Failed to request security: %d", sec_err); + } + + /* Notify main of BLE connection */ + if (ble_cbs && ble_cbs->on_connected) { + ble_cbs->on_connected(); + } +} + +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + char addr[BT_ADDR_LE_STR_LEN]; + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + LOG_DBG("%s reason 0x%02x", addr, reason); + + if (conn == current_conn) { + bt_conn_unref(current_conn); + current_conn = NULL; + } + nus_notif_enabled = false; + + /* Reset BLE TX state */ + ble_tx_in_progress = false; + ble_tx_ready = false; + + /* Clear interface state if BLE was active */ + if (active_iface == ZEPHCORE_IFACE_BLE) { + active_iface = ZEPHCORE_IFACE_NONE; + LOG_INF("active_iface = IFACE_NONE"); + } + + /* Clear queues and retry state */ + k_msgq_purge(&ble_send_queue); + k_msgq_purge(&ble_recv_queue); + tx_retry_pending = false; + + k_work_cancel_delayable(&tx_drain_work); + + /* Notify main of BLE disconnection */ + if (ble_cbs && ble_cbs->on_disconnected) { + ble_cbs->on_disconnected(); + } +} + +/* Flag to suppress recycled() callback during adv mode switch */ +static void recycled(void) +{ + if (adv_switching || adv_is_slow) { + LOG_DBG("suppressed (switching=%d slow=%d)", + adv_switching, adv_is_slow); + return; + } + LOG_DBG("restart advertising"); + start_adv(); +} + +static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + if (err) { + LOG_WRN("security failed: %s level %u err %d", addr, level, err); + return; + } + LOG_INF("%s level %u", addr, level); + + /* For bonded reconnects, pairing_complete is NOT called - only security_changed. + * Enable TX when we have sufficient security (level 2+ = encrypted). + * This handles both fresh pairing (pairing_complete also sets it) and + * reconnects with existing bond. + */ + if (level >= BT_SECURITY_L2 && !ble_tx_ready) { + LOG_INF("security established, enabling TX"); + ble_tx_ready = true; + active_iface = ZEPHCORE_IFACE_BLE; + + /* Request our preferred connection parameters. + * This is the single place for conn param requests - + * fires for both fresh pairing and bonded reconnects. */ + struct bt_le_conn_param conn_param = { + .interval_min = BLE_DEFAULT_MIN_INTERVAL, + .interval_max = BLE_DEFAULT_MAX_INTERVAL, + .latency = BLE_DEFAULT_LATENCY, + .timeout = BLE_DEFAULT_TIMEOUT, + }; + int param_err = bt_conn_le_param_update(conn, &conn_param); + if (param_err) { + LOG_WRN("Failed to request conn param update: %d", param_err); + } else { + LOG_INF("Requested conn params: %d-%dms interval, latency=%d", + BLE_DEFAULT_MIN_INTERVAL * 5 / 4, + BLE_DEFAULT_MAX_INTERVAL * 5 / 4, + BLE_DEFAULT_LATENCY); + } + + /* Don't kick TX here — MTU exchange may not be done yet. + * TX starts when client subscribes to CCC (secure_nus_ccc_changed), + * which is the last step and implies MTU is negotiated. */ + } +} + +static bool le_param_req(struct bt_conn *conn, struct bt_le_conn_param *param) +{ + return true; +} + +static void le_param_updated(struct bt_conn *conn, uint16_t interval, + uint16_t latency, uint16_t timeout) +{ + ARG_UNUSED(conn); + LOG_INF("BLE conn params updated: interval=%dms latency=%d timeout=%dms", + interval * 5 / 4, latency, timeout * 10); +} + +#if defined(CONFIG_BT_USER_DATA_LEN_UPDATE) +static void le_data_len_updated(struct bt_conn *conn, struct bt_conn_le_data_len_info *info) +{ + ARG_UNUSED(conn); + LOG_INF("BLE data length updated: TX=%u/%uus RX=%u/%uus", + info->tx_max_len, info->tx_max_time, + info->rx_max_len, info->rx_max_time); +} +#endif + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = connected, + .disconnected = disconnected, + .recycled = recycled, + .le_param_req = le_param_req, + .le_param_updated = le_param_updated, + .security_changed = security_changed, +#if defined(CONFIG_BT_USER_DATA_LEN_UPDATE) + .le_data_len_updated = le_data_len_updated, +#endif +}; + +/* ========== Authentication callbacks ========== */ + +static uint32_t auth_app_passkey(struct bt_conn *conn) +{ + return ble_passkey; +} + +static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey) +{ + ARG_UNUSED(conn); + ARG_UNUSED(passkey); +} + +static void auth_cancel(struct bt_conn *conn) +{ + ARG_UNUSED(conn); + LOG_WRN("pairing cancelled"); +} + +static struct bt_conn_auth_cb auth_cb = { + .passkey_display = auth_passkey_display, + .cancel = auth_cancel, + .app_passkey = auth_app_passkey, +}; + +static void pairing_complete(struct bt_conn *conn, bool bonded) +{ + ARG_UNUSED(conn); + LOG_DBG("bonded=%d", bonded); + + /* Switch to BLE interface (fresh pairing only). + * Conn params and TX enable are handled in security_changed(), + * which fires for both fresh pairing and bonded reconnects. */ + if (active_iface == ZEPHCORE_IFACE_USB) { + LOG_INF("switching from USB to BLE"); + /* Main handles USB state clearing via on_connected callback */ + } + active_iface = ZEPHCORE_IFACE_BLE; + LOG_INF("active_iface = IFACE_BLE"); +} + +static void pairing_failed(struct bt_conn *conn, enum bt_security_err reason) +{ + LOG_WRN("pairing failed: reason %d", reason); + bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL); +} + +static struct bt_conn_auth_info_cb auth_info_cb = { + .pairing_complete = pairing_complete, + .pairing_failed = pairing_failed, +}; + +/* ========== TX drain work ========== */ + +static void kick_tx_drain(void) +{ + k_work_schedule(&tx_drain_work, K_NO_WAIT); +} + +static void tx_drain_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + struct frame f; + int err; + + /* USB TX path is handled in main — only BLE TX here */ + if (active_iface == ZEPHCORE_IFACE_USB) { + /* Let main handle USB TX — just signal tx_idle if queue empty */ + if (k_msgq_num_used_get(&ble_send_queue) == 0) { + if (ble_cbs && ble_cbs->on_tx_idle) { + ble_cbs->on_tx_idle(); + } + } + return; + } + + /* + * BLE TX path - Event-driven (like Arduino's HVN_TX_COMPLETE) + * Uses bt_gatt_notify_cb() callback to chain TX without polling. + * Re-entrancy guard prevents concurrent notify calls. + * + * IMPORTANT: Must wait for ble_tx_ready before sending. This is set in + * security_changed() after encryption is established. Sending before the + * connection is fully secured causes "No ATT channel for MTU" errors. + */ + if (!current_conn || !nus_notif_enabled || !ble_tx_ready) { + LOG_DBG("tx_drain[BLE]: not ready (conn=%p notif=%d ready=%d)", + current_conn, nus_notif_enabled, ble_tx_ready); + return; + } + + /* Re-entrancy guard - only one TX in flight at a time */ + if (ble_tx_in_progress) { + /* TX timeout watchdog - if callback never fired, reset state */ + if ((k_uptime_get() - ble_tx_start_time) > BLE_TX_TIMEOUT_MS) { + LOG_WRN("tx_drain[BLE]: TX timeout, resetting state"); + ble_tx_in_progress = false; + /* Fall through to try next TX */ + } else { + LOG_DBG("tx_drain[BLE]: TX in progress, callback will chain"); + return; + } + } + + /* Check retry buffer first */ + if (tx_retry_pending) { + LOG_INF("tx_drain[BLE]: retrying len=%u hdr=0x%02x", (unsigned)tx_retry_frame.len, tx_retry_frame.buf[0]); + ble_tx_in_progress = true; + ble_tx_start_time = k_uptime_get(); + err = secure_nus_send(current_conn, tx_retry_frame.buf, tx_retry_frame.len); + if (err == 0) { + tx_retry_pending = false; + LOG_INF("tx_drain[BLE]: retry success"); + return; /* Callback will chain to next */ + } else if (err == -EAGAIN || err == -ENOMEM) { + ble_tx_in_progress = false; + LOG_DBG("tx_drain[BLE]: retry still busy, wait %dms", BLE_TX_RETRY_MS); + k_work_schedule(&tx_drain_work, K_MSEC(BLE_TX_RETRY_MS)); + return; + } else { + ble_tx_in_progress = false; + tx_retry_pending = false; + LOG_WRN("tx_drain[BLE]: retry failed err=%d, dropped", err); + /* Fall through to try next frame */ + } + } + + /* Get next frame from queue */ + if (k_msgq_get(&ble_send_queue, &f, K_NO_WAIT) != 0) { + /* TX queue empty - signal idle */ + if (ble_cbs && ble_cbs->on_tx_idle) { + ble_cbs->on_tx_idle(); + } + return; + } + + LOG_DBG("tx_drain[BLE]: sending len=%u hdr=0x%02x queue=%u", + (unsigned)f.len, f.buf[0], k_msgq_num_used_get(&ble_send_queue)); + + /* Mark TX in progress before calling notify_cb */ + ble_tx_in_progress = true; + ble_tx_start_time = k_uptime_get(); + err = secure_nus_send(current_conn, f.buf, f.len); + + if (err == 0) { + /* Success - callback will chain to next */ + LOG_DBG("tx_drain[BLE]: queued for TX"); + return; + } else if (err == -EAGAIN || err == -ENOMEM) { + /* BLE buffer full - save for retry */ + ble_tx_in_progress = false; + tx_retry_frame = f; + tx_retry_pending = true; + LOG_DBG("tx_drain[BLE]: BLE busy (err=%d), saved for retry", err); + k_work_schedule(&tx_drain_work, K_MSEC(BLE_TX_RETRY_MS)); + return; + } else { + /* Other error - drop frame */ + ble_tx_in_progress = false; + LOG_WRN("tx_drain[BLE]: send failed err=%d, dropped frame", err); + return; + } +} + +/* ========== NUS service callbacks ========== */ + +static void secure_nus_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + ARG_UNUSED(attr); + bool enabled = (value == BT_GATT_CCC_NOTIFY); + LOG_DBG("enabled=%d", enabled); + nus_notif_enabled = enabled; + if (enabled) { + kick_tx_drain(); + } +} + +static ssize_t secure_nus_rx_write(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags) +{ + ARG_UNUSED(conn); + ARG_UNUSED(attr); + ARG_UNUSED(offset); + ARG_UNUSED(flags); + + if (len == 0 || len > MAX_FRAME_SIZE) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + const uint8_t *data = (const uint8_t *)buf; + uint8_t cmd = data[0]; + + LOG_DBG("len=%u cmd=0x%02x", len, cmd); + + /* Notify main via callback */ + if (ble_cbs && ble_cbs->on_rx_frame) { + ble_cbs->on_rx_frame(data, len); + } + + return len; +} + +/* ========== Advertising ========== */ + +static void start_adv(void) +{ + /* Start with fast 20ms advertising for quick discovery */ + struct bt_le_adv_param adv_param = { + .id = BT_ID_DEFAULT, + .options = BT_LE_ADV_OPT_CONN, + .interval_min = BT_ADV_INTERVAL_FAST, + .interval_max = BT_ADV_INTERVAL_FAST, + }; + + adv_is_slow = false; + int err = bt_le_adv_start(&adv_param, ad, ad_len, sd, sd_len); + if (err && err != -EALREADY) { + LOG_ERR("adv start failed: %d", err); + } else { + LOG_INF("BLE advertising: fast mode (20ms) for %ds", BT_ADV_FAST_TIMEOUT_SEC); + /* Schedule switch to slow mode after timeout */ + k_work_schedule(&adv_slow_work, K_SECONDS(BT_ADV_FAST_TIMEOUT_SEC)); + } +} + +static void adv_slow_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + /* Only switch if not connected */ + if (current_conn) { + return; + } + + /* Stop current advertising and restart with slow interval. + * Set flag to prevent recycled() from calling start_adv() again. */ + adv_switching = true; + bt_le_adv_stop(); + + struct bt_le_adv_param adv_param = { + .id = BT_ID_DEFAULT, + .options = BT_LE_ADV_OPT_CONN, + .interval_min = BT_ADV_INTERVAL_SLOW, + .interval_max = BT_ADV_INTERVAL_SLOW, + }; + + int err = bt_le_adv_start(&adv_param, ad, ad_len, sd, sd_len); + adv_switching = false; + if (err && err != -EALREADY) { + LOG_ERR("slow adv start failed: %d", err); + } else { + adv_is_slow = true; + LOG_INF("BLE advertising: slow mode (546ms)"); + } +} + +/* ========== Public API ========== */ + +void zephcore_ble_init(const struct ble_callbacks *cbs) +{ + ble_cbs = cbs; + bt_conn_auth_cb_register(&auth_cb); + bt_conn_auth_info_cb_register(&auth_info_cb); +} + +void zephcore_ble_start(const char *name) +{ + if (IS_ENABLED(CONFIG_SETTINGS)) { + settings_load(); + } + + build_device_name_and_adv(name); + LOG_DBG("init complete, starting adv"); + start_adv(); +} + +size_t zephcore_ble_send(const uint8_t *data, uint16_t len) +{ + if (len == 0 || len > MAX_FRAME_SIZE) { + LOG_WRN("invalid len=%u", (unsigned)len); + return 0; + } + + /* Don't queue frames if no active transport */ + if (active_iface == ZEPHCORE_IFACE_BLE && !current_conn) { + LOG_DBG("no BLE conn, dropping len=%u hdr=0x%02x", + (unsigned)len, data[0]); + return 0; + } + if (active_iface == ZEPHCORE_IFACE_NONE) { + LOG_DBG("no active iface, dropping len=%u hdr=0x%02x", + (unsigned)len, data[0]); + return 0; + } + + struct frame f; + f.len = len; + memcpy(f.buf, data, len); + + if (k_msgq_put(&ble_send_queue, &f, K_NO_WAIT) != 0) { + LOG_WRN("queue full!"); + return 0; + } + + LOG_DBG("queued len=%u hdr=0x%02x queue=%u", + (unsigned)len, data[0], k_msgq_num_used_get(&ble_send_queue)); + + kick_tx_drain(); + return len; +} + +void zephcore_ble_set_enabled(bool enable) +{ + if (!enable) { + /* Disconnect current connection if any */ + if (current_conn) { + bt_conn_disconnect(current_conn, + BT_HCI_ERR_REMOTE_USER_TERM_CONN); + } + /* Stop advertising */ + adv_switching = true; + bt_le_adv_stop(); + adv_switching = false; + adv_is_slow = false; + k_work_cancel_delayable(&adv_slow_work); + LOG_INF("BLE disabled"); + } else { + /* Re-enable advertising */ + start_adv(); + LOG_INF("BLE enabled"); + } +} + +bool zephcore_ble_is_active(void) +{ + return active_iface == ZEPHCORE_IFACE_BLE && current_conn != NULL && ble_tx_ready; +} + +bool zephcore_ble_is_connected(void) +{ + return current_conn != NULL; +} + +void zephcore_ble_set_passkey(uint32_t passkey) +{ + if (passkey >= 100000 && passkey <= 999999) { + ble_passkey = passkey; + } else { + ble_passkey = CONFIG_ZEPHCORE_BLE_PASSKEY; + } + LOG_INF("BLE passkey updated to %06u (effective on next pairing)", ble_passkey); +} + +uint32_t zephcore_ble_get_passkey(void) +{ + return ble_passkey; +} + +enum zephcore_iface zephcore_ble_get_active_iface(void) +{ + return active_iface; +} + +void zephcore_ble_set_active_iface(enum zephcore_iface iface) +{ + active_iface = iface; +} + +struct k_msgq *zephcore_ble_get_recv_queue(void) +{ + return &ble_recv_queue; +} + +struct k_msgq *zephcore_ble_get_send_queue(void) +{ + return &ble_send_queue; +} + +void zephcore_ble_kick_tx(void) +{ + kick_tx_drain(); +} + +void zephcore_ble_disconnect(void) +{ + if (current_conn) { + bt_conn_disconnect(current_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + } +} diff --git a/zephcore/adapters/ble/ZephyrBLE.h b/zephcore/adapters/ble/ZephyrBLE.h new file mode 100644 index 0000000..81425e6 --- /dev/null +++ b/zephcore/adapters/ble/ZephyrBLE.h @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore BLE Adapter — NUS service, advertising, security, TX/RX + */ +#pragma once + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Callbacks from BLE adapter → main */ +struct ble_callbacks { + /* Called when a complete frame is received from the remote (RX). + * Called from system work queue context — must not block. */ + void (*on_rx_frame)(const uint8_t *data, uint16_t len); + /* Called when TX queue is empty (can continue contact iteration) */ + void (*on_tx_idle)(void); + /* Called on BLE connect (for UI notify + USB state clearing) */ + void (*on_connected)(void); + /* Called on BLE disconnect */ + void (*on_disconnected)(void); +}; + +/* Interface type for BLE/USB coexistence */ +enum zephcore_iface { + ZEPHCORE_IFACE_NONE, + ZEPHCORE_IFACE_BLE, + ZEPHCORE_IFACE_USB, +}; + +/** + * Register callbacks and auth handlers. Call before bt_enable(). + */ +void zephcore_ble_init(const struct ble_callbacks *cbs); + +/** + * Called from bt_ready() after bt_enable succeeds. + * Loads settings, builds advertising data, starts advertising. + * @param device_name Name to advertise (NULL = "MeshCore") + */ +void zephcore_ble_start(const char *device_name); + +/** + * Queue a frame for BLE TX. + * @return number of bytes queued, or 0 on failure + */ +size_t zephcore_ble_send(const uint8_t *data, uint16_t len); + +/** + * Enable/disable BLE (advertising + connections). + * When disabled, disconnects any active connection and stops advertising. + */ +void zephcore_ble_set_enabled(bool enable); + +/** + * Check if BLE is the active transport and ready to send. + */ +bool zephcore_ble_is_active(void); + +/** + * Check if BLE has an active connection (regardless of interface state). + */ +bool zephcore_ble_is_connected(void); + +/** + * Set the BLE passkey at runtime. + */ +void zephcore_ble_set_passkey(uint32_t passkey); + +/** + * Get the current BLE passkey. + */ +uint32_t zephcore_ble_get_passkey(void); + +/** + * Get/set active interface (for USB switching in main). + */ +enum zephcore_iface zephcore_ble_get_active_iface(void); +void zephcore_ble_set_active_iface(enum zephcore_iface iface); + +/** + * Get pointer to the recv_queue (k_msgq) for USB RX path sharing. + * USB code in main needs to put frames directly into this queue. + */ +struct k_msgq *zephcore_ble_get_recv_queue(void); + +/** + * Get pointer to the send_queue for USB TX path. + */ +struct k_msgq *zephcore_ble_get_send_queue(void); + +/** + * Kick the TX drain work — call after putting frames in the send queue. + */ +void zephcore_ble_kick_tx(void); + +/** + * Disconnect the current BLE connection (if any). + * Used by USB when it takes over as active interface. + */ +void zephcore_ble_disconnect(void); + +#ifdef __cplusplus +} +#endif diff --git a/zephcore/adapters/board/.gitkeep b/zephcore/adapters/board/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/zephcore/adapters/board/.gitkeep @@ -0,0 +1 @@ + diff --git a/zephcore/adapters/board/ZephyrBoard.cpp b/zephcore/adapters/board/ZephyrBoard.cpp new file mode 100644 index 0000000..fb47c8f --- /dev/null +++ b/zephcore/adapters/board/ZephyrBoard.cpp @@ -0,0 +1,192 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ZephyrBoard.h" +#include +#include +#include +#include +#include + +#if defined(CONFIG_SOC_SERIES_NRF52X) || defined(CONFIG_SOC_SERIES_NRF52) +#include +/* Adafruit bootloader GPREGRET magic values */ +#define BOOTLOADER_DFU_UF2_MAGIC 0x57 /* Enter UF2 mass storage mode */ +#define BOOTLOADER_DFU_OTA_MAGIC 0xA8 /* Enter BLE OTA DFU mode */ +#define NRF52_GPREGRET 1 +#endif + +#include +LOG_MODULE_REGISTER(zephcore_board, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +#if DT_NODE_EXISTS(DT_PATH(zephyr_user)) && \ + DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels) +#include +#include + +/* Battery ADC channel from devicetree zephyr,user { io-channels } */ +static const struct adc_dt_spec vbat_adc = ADC_DT_SPEC_GET(DT_PATH(zephyr_user)); +static bool vbat_adc_configured; + +/* Battery ADC enable regulator (optional - saves power when not reading) */ +#if DT_NODE_EXISTS(DT_NODELABEL(vbat_enable)) +static const struct device *vbat_enable_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(vbat_enable)); +#else +static const struct device *vbat_enable_dev = NULL; +#endif + +/* + * Battery voltage multiplier - prefer devicetree, fallback to Kconfig + * Formula: Battery_mV = (raw * VBAT_MV_MULTIPLIER) / 4096 + * + * To define in devicetree, add to board's DTS/overlay: + * zephyr,user { + * vbat-mv-multiplier = <7200>; + * }; + */ +#define ZEPHYR_USER_NODE DT_PATH(zephyr_user) +#if DT_NODE_HAS_PROP(ZEPHYR_USER_NODE, vbat_mv_multiplier) +#define VBAT_MV_MULTIPLIER DT_PROP(ZEPHYR_USER_NODE, vbat_mv_multiplier) +#else +#define VBAT_MV_MULTIPLIER CONFIG_ZEPHCORE_VBAT_MV_MULTIPLIER +#endif +#define VBAT_ADC_SAMPLES 8 +#endif + +namespace mesh { + +uint16_t ZephyrBoard::getBattMilliVolts() +{ +#if DT_NODE_EXISTS(DT_PATH(zephyr_user)) && \ + DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels) + if (!adc_is_ready_dt(&vbat_adc)) { + LOG_ERR("ADC not ready"); + return 0; + } + + if (!vbat_adc_configured) { + int ret = adc_channel_setup_dt(&vbat_adc); + if (ret < 0) { + LOG_ERR("ADC channel setup failed: %d", ret); + return 0; + } + vbat_adc_configured = true; + } + + /* Enable battery ADC voltage divider (saves power when not reading) */ + if (vbat_enable_dev && device_is_ready(vbat_enable_dev)) { + regulator_enable(vbat_enable_dev); + k_msleep(10); /* 10ms settling time for voltage divider + capacitor (matches Arduino) */ + } + + int32_t raw = 0; + int valid_samples = 0; + for (int i = 0; i < VBAT_ADC_SAMPLES; i++) { + int16_t val = 0; /* Use int16_t for 12-bit ADC */ + struct adc_sequence seq = { + .buffer = &val, + .buffer_size = sizeof(val), + }; + int ret = adc_sequence_init_dt(&vbat_adc, &seq); + if (ret < 0) { + LOG_WRN("ADC sequence init failed: %d", ret); + continue; + } + ret = adc_read_dt(&vbat_adc, &seq); + if (ret == 0) { + raw += val; + valid_samples++; + } else { + LOG_WRN("ADC read failed: %d", ret); + } + } + + /* Disable battery ADC voltage divider to save power */ + if (vbat_enable_dev && device_is_ready(vbat_enable_dev)) { + regulator_disable(vbat_enable_dev); + } + + if (valid_samples == 0) { + LOG_ERR("No valid ADC samples"); + return 0; + } + raw /= valid_samples; + uint16_t mv = (uint16_t)((VBAT_MV_MULTIPLIER * (int64_t)raw) / 4096); + LOG_INF("Battery: raw=%d multiplier=%d mv=%u", (int)raw, VBAT_MV_MULTIPLIER, mv); + return mv; +#else + return 0; +#endif +} + +float ZephyrBoard::getMCUTemperature() +{ + /* nRF52840 die temperature sensor - "nordic,nrf-temp" at 0x4000c000 + * Nodelabel "temp" is defined in nrf52840.dtsi, status="okay" by default */ + const struct device *dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(temp)); + if (!dev || !device_is_ready(dev)) { + return NAN; + } + struct sensor_value val; + if (sensor_sample_fetch(dev) == 0 && + sensor_channel_get(dev, SENSOR_CHAN_DIE_TEMP, &val) == 0) { + return sensor_value_to_float(&val); + } + return NAN; +} + +const char *ZephyrBoard::getManufacturerName() const +{ + return CONFIG_ZEPHCORE_BOARD_NAME; +} + +void ZephyrBoard::reboot() +{ + k_msleep(50); /* Let UART/USB flush */ + sys_reboot(SYS_REBOOT_COLD); +} + +void ZephyrBoard::rebootToBootloader() +{ +#ifdef NRF52_GPREGRET + /* Write magic value to GPREGRET0 - enter UF2 mass storage mode */ + nrf_power_gpregret_set(NRF_POWER, 0, BOOTLOADER_DFU_UF2_MAGIC); +#endif + k_msleep(50); /* Let UART/USB flush */ + sys_reboot(SYS_REBOOT_COLD); +} + +bool ZephyrBoard::startOTAUpdate(const char *id, char reply[]) +{ +#ifdef NRF52_GPREGRET + /* Write magic value to GPREGRET0 - enter BLE OTA DFU mode */ + nrf_power_gpregret_set(NRF_POWER, 0, BOOTLOADER_DFU_OTA_MAGIC); + sprintf(reply, "OK - rebooting to BLE DFU (name: %s)", id ? id : "DfuTarg"); + k_msleep(50); /* Let UART/USB flush */ + sys_reboot(SYS_REBOOT_COLD); + return true; /* Never reached */ +#else + (void)id; + strcpy(reply, "Error: BLE OTA not supported on this platform"); + return false; +#endif +} + +void ZephyrBoard::clearBootloaderMagic() +{ +#ifdef NRF52_GPREGRET + /* Clear any stale GPREGRET values from previous sessions. + * GPREGRET0: bootloader DFU mode select (0x57=UF2, 0xA8=OTA) + * GPREGRET1: wake gate / deep sleep flag */ + nrf_power_gpregret_set(NRF_POWER, 0, 0x00); + nrf_power_gpregret_set(NRF_POWER, 1, 0x00); +#endif +} + +uint8_t ZephyrBoard::getStartupReason() const +{ + return BD_STARTUP_NORMAL; +} + +} /* namespace mesh */ diff --git a/zephcore/adapters/board/ZephyrBoard.h b/zephcore/adapters/board/ZephyrBoard.h new file mode 100644 index 0000000..c43b9bc --- /dev/null +++ b/zephcore/adapters/board/ZephyrBoard.h @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Zephyr MainBoard base - nRF52840 DK and generic + */ + +#pragma once + +#include + +namespace mesh { + +class ZephyrBoard : public MainBoard { +public: + uint16_t getBattMilliVolts() override; + float getMCUTemperature() override; + const char *getManufacturerName() const override; + void reboot() override; + void rebootToBootloader(); /* Reboot into UF2 mass storage bootloader */ + bool startOTAUpdate(const char *id, char reply[]) override; /* Reboot into BLE OTA DFU */ + void clearBootloaderMagic(); /* Clear stale GPREGRET values at startup */ + uint8_t getStartupReason() const override; +}; + +} /* namespace mesh */ diff --git a/zephcore/adapters/clock/.gitkeep b/zephcore/adapters/clock/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/zephcore/adapters/clock/.gitkeep @@ -0,0 +1 @@ + diff --git a/zephcore/adapters/clock/ZephyrMillisecondClock.cpp b/zephcore/adapters/clock/ZephyrMillisecondClock.cpp new file mode 100644 index 0000000..f0cfc4c --- /dev/null +++ b/zephcore/adapters/clock/ZephyrMillisecondClock.cpp @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ZephyrMillisecondClock.h" +#include + +namespace mesh { + +unsigned long ZephyrMillisecondClock::getMillis() +{ + return (unsigned long)k_uptime_get_32(); +} + +} /* namespace mesh */ diff --git a/zephcore/adapters/clock/ZephyrMillisecondClock.h b/zephcore/adapters/clock/ZephyrMillisecondClock.h new file mode 100644 index 0000000..0add706 --- /dev/null +++ b/zephcore/adapters/clock/ZephyrMillisecondClock.h @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Zephyr implementation of MillisecondClock + */ + +#pragma once + +#include + +namespace mesh { + +class ZephyrMillisecondClock : public MillisecondClock { +public: + unsigned long getMillis() override; +}; + +} /* namespace mesh */ diff --git a/zephcore/adapters/clock/ZephyrRTCClock.cpp b/zephcore/adapters/clock/ZephyrRTCClock.cpp new file mode 100644 index 0000000..fe56a81 --- /dev/null +++ b/zephcore/adapters/clock/ZephyrRTCClock.cpp @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ZephyrRTCClock.h" +#include + +namespace mesh { + +uint32_t ZephyrRTCClock::getCurrentTime() +{ + uint32_t uptime_sec = (uint32_t)(k_uptime_get() / 1000); + return epoch_offset + uptime_sec; +} + +void ZephyrRTCClock::setCurrentTime(uint32_t time) +{ + uint32_t uptime_sec = (uint32_t)(k_uptime_get() / 1000); + epoch_offset = time - uptime_sec; +} + +} /* namespace mesh */ diff --git a/zephcore/adapters/clock/ZephyrRTCClock.h b/zephcore/adapters/clock/ZephyrRTCClock.h new file mode 100644 index 0000000..68c14dc --- /dev/null +++ b/zephcore/adapters/clock/ZephyrRTCClock.h @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Zephyr software RTC - boot-relative time with settable offset + */ + +#pragma once + +#include + +namespace mesh { + +class ZephyrRTCClock : public RTCClock { +public: + uint32_t getCurrentTime() override; + void setCurrentTime(uint32_t time) override; + +private: + uint32_t epoch_offset = 0; +}; + +} /* namespace mesh */ diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp new file mode 100644 index 0000000..622846d --- /dev/null +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -0,0 +1,739 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Zephyr DataStore - LittleFS-backed persistence with optional QSPI flash + */ + +#include "ZephyrDataStore.h" +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_datastore, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); + +#define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE) + +struct BlobRec { + uint32_t timestamp; + uint8_t key[7]; + uint8_t len; + uint8_t data[MAX_ADVERT_PKT_LEN]; +}; + +/* Track mount status - filesystems are automounted via DTS fstab */ +static bool lfs_mounted; +static bool ext_lfs_mounted; + +/* Check if a filesystem is mounted using fs_statvfs */ +static bool is_mounted(const char *mount_point) +{ + struct fs_statvfs stat; + return fs_statvfs(mount_point, &stat) == 0; +} + +bool ZephyrDataStore::mount() +{ + if (lfs_mounted) { + return true; + } + + /* Check if internal LFS was automounted */ + if (is_mounted(mountPoint())) { + lfs_mounted = true; + LOG_INF("Internal LittleFS at %s (automounted)", mountPoint()); + } else { + LOG_ERR("Internal LittleFS NOT mounted at %s - check DTS fstab!", mountPoint()); + return false; + } + + /* Check if external QSPI was automounted */ + if (is_mounted(extMountPoint())) { + ext_lfs_mounted = true; + LOG_INF("External QSPI LittleFS at %s (automounted, 100 blobs)", extMountPoint()); + } else { + ext_lfs_mounted = false; + LOG_WRN("External QSPI NOT mounted at %s - using internal only (20 blobs)", extMountPoint()); + } + + return true; +} + +void ZephyrDataStore::unmount() +{ + /* With automount, filesystems are managed by Zephyr - just clear our flags */ + lfs_mounted = false; + ext_lfs_mounted = false; +} + +ZephyrDataStore::ZephyrDataStore(mesh::RTCClock &clock) + : _clock(&clock), _has_ext_fs(false) +{ +} + +void ZephyrDataStore::begin() +{ + _has_ext_fs = ext_lfs_mounted; + LOG_INF("_has_ext_fs=%d (ext_lfs_mounted=%d)", _has_ext_fs ? 1 : 0, ext_lfs_mounted ? 1 : 0); + LOG_INF("contacts path=%s, channels path=%s", contactsFile(), channelsFile()); + + if (_has_ext_fs) { + migrateToExternalFS(); + } + + checkAdvBlobFile(); +} + +bool ZephyrDataStore::exists(const char *path) +{ + struct fs_dirent ent; + return fs_stat(path, &ent) == 0; +} + +bool ZephyrDataStore::removeFile(const char *path) +{ + return fs_unlink(path) == 0; +} + +bool ZephyrDataStore::openRead(const char *path, uint8_t *buf, size_t buf_sz, size_t &out_len) +{ + struct fs_file_t file; + fs_file_t_init(&file); + int rc = fs_open(&file, path, FS_O_READ); + if (rc < 0) { + return false; + } + ssize_t n = fs_read(&file, buf, buf_sz); + fs_close(&file); + if (n < 0) { + return false; + } + out_len = (size_t)n; + return true; +} + +bool ZephyrDataStore::openWrite(const char *path, const uint8_t *buf, size_t len) +{ + fs_unlink(path); + + struct fs_file_t file; + fs_file_t_init(&file); + int rc = fs_open(&file, path, FS_O_CREATE | FS_O_WRITE); + if (rc < 0) { + LOG_ERR("openWrite: fs_open(%s) failed: %d", path, rc); + return false; + } + ssize_t n = fs_write(&file, buf, len); + fs_close(&file); + return n >= 0 && (size_t)n == len; +} + +bool ZephyrDataStore::copyFile(const char *src, const char *dst) +{ + struct fs_file_t src_file, dst_file; + fs_file_t_init(&src_file); + fs_file_t_init(&dst_file); + + if (fs_open(&src_file, src, FS_O_READ) < 0) { + return false; + } + + fs_unlink(dst); + if (fs_open(&dst_file, dst, FS_O_CREATE | FS_O_WRITE) < 0) { + fs_close(&src_file); + return false; + } + + uint8_t buf[64]; + ssize_t n; + bool ok = true; + while ((n = fs_read(&src_file, buf, sizeof(buf))) > 0) { + if (fs_write(&dst_file, buf, n) != n) { + ok = false; + break; + } + } + + fs_close(&src_file); + fs_close(&dst_file); + return ok && n >= 0; +} + +void ZephyrDataStore::migrateToExternalFS() +{ + /* Migrate contacts from internal to external if not present */ + if (!exists(EXT_CONTACTS_FILE) && exists(INT_CONTACTS_FILE)) { + LOG_INF("Migrating contacts to external storage"); + if (copyFile(INT_CONTACTS_FILE, EXT_CONTACTS_FILE)) { + removeFile(INT_CONTACTS_FILE); + } + } + + /* Migrate channels from internal to external if not present */ + if (!exists(EXT_CHANNELS_FILE) && exists(INT_CHANNELS_FILE)) { + LOG_INF("Migrating channels to external storage"); + if (copyFile(INT_CHANNELS_FILE, EXT_CHANNELS_FILE)) { + removeFile(INT_CHANNELS_FILE); + } + } + + /* Migrate adv_blobs - need special handling for size change */ + if (!exists(EXT_ADV_BLOBS_FILE) && exists(INT_ADV_BLOBS_FILE)) { + LOG_INF("Migrating adv_blobs to external storage (20 -> 100 slots)"); + if (copyFile(INT_ADV_BLOBS_FILE, EXT_ADV_BLOBS_FILE)) { + removeFile(INT_ADV_BLOBS_FILE); + /* Extend blob file to 100 records */ + struct fs_file_t file; + fs_file_t_init(&file); + if (fs_open(&file, EXT_ADV_BLOBS_FILE, FS_O_RDWR) == 0) { + fs_seek(&file, 0, FS_SEEK_END); + BlobRec zeroes; + memset(&zeroes, 0, sizeof(zeroes)); + for (int i = 20; i < 100; i++) { + fs_write(&file, &zeroes, sizeof(zeroes)); + } + fs_close(&file); + } + } + } + + /* Clean up old files on internal if they exist on external */ + if (exists(EXT_CONTACTS_FILE) && exists(INT_CONTACTS_FILE)) { + removeFile(INT_CONTACTS_FILE); + } + if (exists(EXT_CHANNELS_FILE) && exists(INT_CHANNELS_FILE)) { + removeFile(INT_CHANNELS_FILE); + } + if (exists(EXT_ADV_BLOBS_FILE) && exists(INT_ADV_BLOBS_FILE)) { + removeFile(INT_ADV_BLOBS_FILE); + } +} + +void ZephyrDataStore::checkAdvBlobFile() +{ + const char *path = advBlobsFile(); + if (exists(path)) { + return; + } + BlobRec zeroes; + memset(&zeroes, 0, sizeof(zeroes)); + struct fs_file_t file; + fs_file_t_init(&file); + int rc = fs_open(&file, path, FS_O_CREATE | FS_O_WRITE); + if (rc < 0) { + LOG_ERR("Failed to create adv_blobs file: %d", rc); + return; + } + int count = maxBlobRecs(); + for (int i = 0; i < count; i++) { + fs_write(&file, &zeroes, sizeof(zeroes)); + } + fs_close(&file); + LOG_INF("Created adv_blobs with %d slots", count); +} + +bool ZephyrDataStore::formatFileSystem() +{ + LOG_INF("formatFileSystem: starting..."); + unmount(); + + const struct flash_area *fap; + int rc; + + /* Format internal LittleFS partition */ + LOG_INF("formatFileSystem: opening lfs_partition..."); + rc = flash_area_open(FIXED_PARTITION_ID(lfs_partition), &fap); + if (rc < 0) { + LOG_ERR("Failed to open lfs_partition: rc=%d", rc); + return false; + } + LOG_INF("formatFileSystem: lfs_partition opened, size=%u, erasing...", (unsigned)fap->fa_size); + rc = flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + if (rc < 0) { + LOG_ERR("Failed to format lfs_partition: rc=%d", rc); + return false; + } + LOG_INF("Formatted internal LittleFS (92KB)"); + + /* Format NVS storage partition (BLE bonds) */ + LOG_INF("formatFileSystem: opening storage_partition (NVS/bonds)..."); + rc = flash_area_open(FIXED_PARTITION_ID(storage_partition), &fap); + if (rc < 0) { + LOG_ERR("Failed to open storage_partition: rc=%d", rc); + return false; + } + LOG_INF("formatFileSystem: storage_partition opened, size=%u, erasing...", (unsigned)fap->fa_size); + rc = flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + if (rc < 0) { + LOG_ERR("Failed to format storage_partition: rc=%d", rc); + return false; + } + LOG_INF("Formatted NVS storage (8KB) - BLE bonds cleared"); + + /* NOTE: We intentionally do NOT erase the bootloader area (0xED000-end). + * Storage partition (0xD4000-0xD6000) contains BLE bonds and IS erased. */ + +#if FIXED_PARTITION_EXISTS(qspi_storage_partition) + /* Format external QSPI flash if present */ + LOG_INF("formatFileSystem: opening qspi_storage_partition..."); + rc = flash_area_open(FIXED_PARTITION_ID(qspi_storage_partition), &fap); + if (rc == 0) { + LOG_INF("formatFileSystem: QSPI partition opened, size=%u, erasing (this may take a while)...", (unsigned)fap->fa_size); + rc = flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + if (rc < 0) { + LOG_ERR("Failed to format external QSPI storage: rc=%d", rc); + } else { + LOG_INF("Formatted external QSPI storage"); + } + } else { + LOG_WRN("formatFileSystem: couldn't open qspi_storage_partition: rc=%d (may not be present)", rc); + } +#else + LOG_INF("formatFileSystem: no qspi_storage_partition defined, skipping QSPI"); +#endif + + LOG_INF("formatFileSystem: calling mount()..."); + bool mounted = mount(); + LOG_INF("formatFileSystem: mount() returned %d", mounted ? 1 : 0); + return mounted; +} + +bool ZephyrDataStore::loadMainIdentity(mesh::LocalIdentity &identity) +{ + uint8_t buf[PRV_KEY_SIZE + PUB_KEY_SIZE + 32]; + size_t len = 0; + if (!openRead(MAIN_ID_FILE, buf, sizeof(buf), len) || len < PRV_KEY_SIZE + PUB_KEY_SIZE) { + return false; + } + return identity.readFrom(buf, len); +} + +bool ZephyrDataStore::saveMainIdentity(const mesh::LocalIdentity &identity) +{ + uint8_t buf[PRV_KEY_SIZE + PUB_KEY_SIZE + 32]; + size_t n = identity.writeTo(buf, sizeof(buf)); + if (n == 0) { + return false; + } + return openWrite(MAIN_ID_FILE, buf, n); +} + +void ZephyrDataStore::loadPrefs(NodePrefs &prefs) +{ + bool prefs_exists = exists(PREFS_FILE); + LOG_INF("loadPrefs: exists(%s)=%d", PREFS_FILE, prefs_exists ? 1 : 0); + const char *path = prefs_exists ? PREFS_FILE : "/lfs/node_prefs"; + LOG_INF("loadPrefs: trying path=%s", path); + uint8_t buf[256]; + size_t len = 0; + if (!openRead(path, buf, sizeof(buf), len)) { + LOG_WRN("loadPrefs: file not found or read failed"); + return; + } + if (len < 88) { + LOG_WRN("loadPrefs: file too small (%d bytes, need 88)", (int)len); + return; + } + LOG_INF("loadPrefs: loaded %d bytes from %s", (int)len, path); + size_t off = 0; + memcpy(&prefs.airtime_factor, &buf[off], sizeof(float)); + off += 4; + /* Migrate old AF multiplier (0-9) to duty cycle percentage (0-99) */ + if (prefs.airtime_factor > 0.0f && prefs.airtime_factor <= 9.0f) { + prefs.airtime_factor *= 10.0f; + } + memcpy(prefs.node_name, &buf[off], 32); + off += 36; + memcpy(&prefs.node_lat, &buf[off], sizeof(double)); + off += 8; + memcpy(&prefs.node_lon, &buf[off], sizeof(double)); + off += 8; + memcpy(&prefs.freq, &buf[off], sizeof(float)); + off += 4; + prefs.sf = buf[off++]; + prefs.cr = buf[off++]; + off++; + prefs.manual_add_contacts = buf[off++]; + memcpy(&prefs.bw, &buf[off], sizeof(float)); + off += 4; + prefs.tx_power_dbm = buf[off++]; + prefs.telemetry_mode_base = buf[off++]; + prefs.telemetry_mode_loc = buf[off++]; + prefs.telemetry_mode_env = buf[off++]; + memcpy(&prefs.rx_delay_base, &buf[off], sizeof(float)); + off += 4; + prefs.advert_loc_policy = buf[off++]; + prefs.multi_acks = buf[off++]; + off += 2; + memcpy(&prefs.ble_pin, &buf[off], sizeof(uint32_t)); + off += 4; + prefs.buzzer_quiet = buf[off++]; + prefs.gps_enabled = buf[off++]; + memcpy(&prefs.gps_interval, &buf[off], sizeof(uint32_t)); + off += 4; + prefs.autoadd_config = buf[off++]; + + /* rx_boost - added later, may not exist in old files */ + if (off < len) { + prefs.rx_boost = buf[off++]; + } else { + prefs.rx_boost = 1; /* Default to boosted for better sensitivity */ + } + + /* client_repeat (offgrid mode) - added v9, may not exist in old files */ + if (off < len) { + prefs.client_repeat = buf[off++]; + } else { + prefs.client_repeat = 0; /* Default: disabled (companion only) */ + } + + if (strcmp(path, "/lfs/node_prefs") == 0) { + savePrefs(prefs); + removeFile("/lfs/node_prefs"); + } +} + +void ZephyrDataStore::savePrefs(const NodePrefs &prefs) +{ + uint8_t buf[256]; + uint8_t pad[8] = {0}; + size_t off = 0; + memcpy(&buf[off], &prefs.airtime_factor, sizeof(float)); + off += 4; + memcpy(&buf[off], prefs.node_name, 32); + off += 32; + memcpy(&buf[off], pad, 4); + off += 4; + memcpy(&buf[off], &prefs.node_lat, sizeof(double)); + off += 8; + memcpy(&buf[off], &prefs.node_lon, sizeof(double)); + off += 8; + memcpy(&buf[off], &prefs.freq, sizeof(float)); + off += 4; + buf[off++] = prefs.sf; + buf[off++] = prefs.cr; + buf[off++] = 0; + buf[off++] = prefs.manual_add_contacts; + memcpy(&buf[off], &prefs.bw, sizeof(float)); + off += 4; + buf[off++] = prefs.tx_power_dbm; + buf[off++] = prefs.telemetry_mode_base; + buf[off++] = prefs.telemetry_mode_loc; + buf[off++] = prefs.telemetry_mode_env; + memcpy(&buf[off], &prefs.rx_delay_base, sizeof(float)); + off += 4; + buf[off++] = prefs.advert_loc_policy; + buf[off++] = prefs.multi_acks; + memcpy(&buf[off], pad, 2); + off += 2; + memcpy(&buf[off], &prefs.ble_pin, sizeof(uint32_t)); + off += 4; + buf[off++] = prefs.buzzer_quiet; + buf[off++] = prefs.gps_enabled; + memcpy(&buf[off], &prefs.gps_interval, sizeof(uint32_t)); + off += 4; + buf[off++] = prefs.autoadd_config; + buf[off++] = prefs.rx_boost; + buf[off++] = prefs.client_repeat; + bool ok = openWrite(PREFS_FILE, buf, off); + LOG_INF("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'", PREFS_FILE, ok ? 1 : 0, (int)off, prefs.node_name); +} + +void ZephyrDataStore::loadContacts(DataStoreHost *host) +{ + const char *path = contactsFile(); + LOG_INF("loadContacts: path=%s (_has_ext_fs=%d)", path, _has_ext_fs ? 1 : 0); + + /* Check if file exists and its size */ + struct fs_dirent ent; + if (fs_stat(path, &ent) == 0) { + LOG_INF("loadContacts: file exists, size=%zu bytes", ent.size); + } else { + LOG_WRN("loadContacts: file does not exist at %s", path); + } + + struct fs_file_t file; + fs_file_t_init(&file); + int rc = fs_open(&file, path, FS_O_READ); + if (rc < 0) { + LOG_WRN("loadContacts: fs_open failed (rc=%d)", rc); + return; + } + uint32_t count = 0; + for (;;) { + ContactInfo c; + uint8_t pub_key[32]; + uint8_t unused; + ssize_t n = fs_read(&file, pub_key, 32); + if (n != 32) break; + n = fs_read(&file, (uint8_t *)&c.name, 32); + if (n != 32) break; + n = fs_read(&file, &c.type, 1); + if (n != 1) break; + n = fs_read(&file, &c.flags, 1); + if (n != 1) break; + n = fs_read(&file, &unused, 1); + if (n != 1) break; + n = fs_read(&file, (uint8_t *)&c.sync_since, 4); + if (n != 4) break; + n = fs_read(&file, (uint8_t *)&c.out_path_len, 1); + if (n != 1) break; + n = fs_read(&file, (uint8_t *)&c.last_advert_timestamp, 4); + if (n != 4) break; + n = fs_read(&file, c.out_path, 64); + if (n != 64) break; + n = fs_read(&file, (uint8_t *)&c.lastmod, 4); + if (n != 4) break; + n = fs_read(&file, (uint8_t *)&c.gps_lat, 4); + if (n != 4) break; + n = fs_read(&file, (uint8_t *)&c.gps_lon, 4); + if (n != 4) break; + c.id = mesh::Identity(pub_key); + c.shared_secret_valid = false; + if (!host->onContactLoaded(c)) break; + count++; + } + fs_close(&file); + LOG_INF("loadContacts: loaded %u contacts from %s", count, path); +} + +void ZephyrDataStore::saveContacts(DataStoreHost *host) +{ + const char *path = contactsFile(); + LOG_INF("saveContacts: path=%s (_has_ext_fs=%d)", path, _has_ext_fs ? 1 : 0); + struct fs_file_t file; + fs_file_t_init(&file); + if (exists(path)) { + fs_unlink(path); + } + int rc = fs_open(&file, path, FS_O_CREATE | FS_O_WRITE); + if (rc < 0) { + LOG_ERR("saveContacts: fs_open(%s) failed: %d", path, rc); + return; + } + uint32_t idx = 0; + ContactInfo c; + uint8_t unused = 0; + while (host->getContactForSave(idx, c)) { + if (fs_write(&file, c.id.pub_key, 32) != 32) break; + if (fs_write(&file, (uint8_t *)&c.name, 32) != 32) break; + if (fs_write(&file, &c.type, 1) != 1) break; + if (fs_write(&file, &c.flags, 1) != 1) break; + if (fs_write(&file, &unused, 1) != 1) break; + if (fs_write(&file, (uint8_t *)&c.sync_since, 4) != 4) break; + if (fs_write(&file, (uint8_t *)&c.out_path_len, 1) != 1) break; + if (fs_write(&file, (uint8_t *)&c.last_advert_timestamp, 4) != 4) break; + if (fs_write(&file, c.out_path, 64) != 64) break; + if (fs_write(&file, (uint8_t *)&c.lastmod, 4) != 4) break; + if (fs_write(&file, (uint8_t *)&c.gps_lat, 4) != 4) break; + if (fs_write(&file, (uint8_t *)&c.gps_lon, 4) != 4) break; + idx++; + } + int sync_rc = fs_sync(&file); + fs_close(&file); + LOG_INF("saveContacts: saved %u contacts to %s (sync=%d)", idx, path, sync_rc); +} + +void ZephyrDataStore::loadChannels(DataStoreHost *host) +{ + const char *path = channelsFile(); + struct fs_file_t file; + fs_file_t_init(&file); + if (fs_open(&file, path, FS_O_READ) < 0) { + return; + } + uint8_t channel_idx = 0; + for (;;) { + ChannelDetails ch; + uint8_t unused[4]; + ssize_t n = fs_read(&file, unused, 4); + if (n != 4) break; + n = fs_read(&file, (uint8_t *)ch.name, 32); + if (n != 32) break; + n = fs_read(&file, (uint8_t *)ch.channel.secret, 32); + if (n != 32) break; + if (host->onChannelLoaded(channel_idx, ch)) { + channel_idx++; + } else { + break; + } + } + fs_close(&file); +} + +void ZephyrDataStore::saveChannels(DataStoreHost *host) +{ + const char *path = channelsFile(); + struct fs_file_t file; + fs_file_t_init(&file); + if (exists(path)) { + fs_unlink(path); + } + if (fs_open(&file, path, FS_O_CREATE | FS_O_WRITE) < 0) { + LOG_ERR("Failed to open %s for write", path); + return; + } + uint8_t channel_idx = 0; + ChannelDetails ch; + uint8_t unused[4] = {0}; + while (host->getChannelForSave(channel_idx, ch)) { + if (fs_write(&file, unused, 4) != 4) break; + if (fs_write(&file, (uint8_t *)ch.name, 32) != 32) break; + if (fs_write(&file, (uint8_t *)ch.channel.secret, 32) != 32) break; + channel_idx++; + } + fs_close(&file); + LOG_DBG("Saved %u channels to %s", channel_idx, path); +} + +uint8_t ZephyrDataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) +{ + (void)key_len; + const char *path = advBlobsFile(); + struct fs_file_t file; + fs_file_t_init(&file); + if (fs_open(&file, path, FS_O_READ) < 0) { + return 0; + } + BlobRec tmp; + uint8_t len = 0; + while (fs_read(&file, (uint8_t *)&tmp, sizeof(tmp)) == (ssize_t)sizeof(tmp)) { + if (memcmp(key, tmp.key, 7) == 0) { + len = tmp.len; + memcpy(dest_buf, tmp.data, len); + break; + } + } + fs_close(&file); + return len; +} + +bool ZephyrDataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) +{ + (void)key_len; + if (len < PUB_KEY_SIZE + 4 + SIGNATURE_SIZE || len > MAX_ADVERT_PKT_LEN) { + return false; + } + checkAdvBlobFile(); + const char *path = advBlobsFile(); + struct fs_file_t file; + fs_file_t_init(&file); + if (fs_open(&file, path, FS_O_RDWR) < 0) { + return false; + } + uint32_t pos = 0, found_pos = 0; + uint32_t min_timestamp = 0xFFFFFFFF; + BlobRec tmp; + while (fs_read(&file, (uint8_t *)&tmp, sizeof(tmp)) == (ssize_t)sizeof(tmp)) { + if (memcmp(key, tmp.key, 7) == 0) { + found_pos = pos; + break; + } + if (tmp.timestamp < min_timestamp) { + min_timestamp = tmp.timestamp; + found_pos = pos; + } + pos += sizeof(tmp); + } + memcpy(tmp.key, key, 7); + memcpy(tmp.data, src_buf, len); + tmp.len = len; + tmp.timestamp = _clock->getCurrentTime(); + fs_seek(&file, found_pos, FS_SEEK_SET); + fs_write(&file, (uint8_t *)&tmp, sizeof(tmp)); + fs_close(&file); + return true; +} + +bool ZephyrDataStore::deleteBlobByKey(const uint8_t key[], int key_len) +{ + (void)key_len; + const char *path = advBlobsFile(); + struct fs_file_t file; + fs_file_t_init(&file); + if (fs_open(&file, path, FS_O_RDWR) < 0) { + return true; // no blob file = nothing to delete + } + BlobRec tmp; + uint32_t pos = 0; + while (fs_read(&file, (uint8_t *)&tmp, sizeof(tmp)) == (ssize_t)sizeof(tmp)) { + if (memcmp(key, tmp.key, 7) == 0) { + /* Zero out the record to mark as free */ + memset(&tmp, 0, sizeof(tmp)); + fs_seek(&file, pos, FS_SEEK_SET); + fs_write(&file, (uint8_t *)&tmp, sizeof(tmp)); + break; + } + pos += sizeof(tmp); + } + fs_close(&file); + return true; +} + +uint32_t ZephyrDataStore::getStorageUsedKb() const +{ + struct fs_statvfs sbuf; + uint32_t used = 0; + + /* Internal flash */ + if (fs_statvfs(mountPoint(), &sbuf) == 0) { + uint32_t total = sbuf.f_blocks * sbuf.f_frsize; + uint32_t free = sbuf.f_bfree * sbuf.f_frsize; + used += (total - free) / 1024; + } + + /* External flash if available */ + if (_has_ext_fs && fs_statvfs(extMountPoint(), &sbuf) == 0) { + uint32_t total = sbuf.f_blocks * sbuf.f_frsize; + uint32_t free = sbuf.f_bfree * sbuf.f_frsize; + used += (total - free) / 1024; + } + + return used; +} + +uint32_t ZephyrDataStore::getStorageTotalKb() const +{ + struct fs_statvfs sbuf; + uint32_t total = 0; + + /* Internal flash */ + if (fs_statvfs(mountPoint(), &sbuf) == 0) { + total += (sbuf.f_blocks * sbuf.f_frsize) / 1024; + } + + /* External flash if available */ + if (_has_ext_fs && fs_statvfs(extMountPoint(), &sbuf) == 0) { + total += (sbuf.f_blocks * sbuf.f_frsize) / 1024; + } + + return total; +} + +uint32_t ZephyrDataStore::getExternalStorageKb() const +{ + if (!_has_ext_fs) { + return 0; + } + struct fs_statvfs sbuf; + if (fs_statvfs(extMountPoint(), &sbuf) < 0) { + return 0; + } + return (sbuf.f_blocks * sbuf.f_frsize) / 1024; +} + +void ZephyrDataStore::factoryReset() +{ + LOG_INF("=== FACTORY RESET STARTING ==="); + + /* Format both internal and external (QSPI) filesystems */ + if (formatFileSystem()) { + LOG_INF("=== FACTORY RESET COMPLETE - REBOOT REQUIRED ==="); + } else { + LOG_ERR("=== FACTORY RESET FAILED ==="); + } +} diff --git a/zephcore/adapters/datastore/ZephyrDataStore.h b/zephcore/adapters/datastore/ZephyrDataStore.h new file mode 100644 index 0000000..ac36e75 --- /dev/null +++ b/zephcore/adapters/datastore/ZephyrDataStore.h @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Zephyr DataStore - LittleFS-backed persistence with optional QSPI flash + */ + +#pragma once + +#include +#include +#include +#include +#include + +class DataStoreHost { +public: + virtual bool onContactLoaded(const ContactInfo &contact) = 0; + virtual bool getContactForSave(uint32_t idx, ContactInfo &contact) = 0; + virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails &ch) = 0; + virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails &ch) = 0; +}; + +class ZephyrDataStore { +public: + explicit ZephyrDataStore(mesh::RTCClock &clock); + void begin(); + bool formatFileSystem(); + bool loadMainIdentity(mesh::LocalIdentity &identity); + bool saveMainIdentity(const mesh::LocalIdentity &identity); + void loadPrefs(NodePrefs &prefs); + void savePrefs(const NodePrefs &prefs); + void loadContacts(DataStoreHost *host); + void saveContacts(DataStoreHost *host); + void loadChannels(DataStoreHost *host); + void saveChannels(DataStoreHost *host); + uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]); + bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len); + bool deleteBlobByKey(const uint8_t key[], int key_len); + uint32_t getStorageUsedKb() const; + uint32_t getStorageTotalKb() const; + + /* Factory reset - delete all stored data */ + void factoryReset(); + + /* Check if external QSPI flash is available */ + bool hasExternalStorage() const { return _has_ext_fs; } + uint32_t getExternalStorageKb() const; + + static bool mount(); + static void unmount(); + static const char *mountPoint() { return MNT_POINT; } + static const char *extMountPoint() { return EXT_MNT_POINT; } + +private: + /* Internal flash (always available) - identity, prefs */ + static constexpr const char *MNT_POINT = "/lfs"; + static constexpr const char *PREFS_FILE = "/lfs/new_prefs"; + static constexpr const char *MAIN_ID_FILE = "/lfs/_main.id"; + + /* External QSPI flash (optional) - contacts, channels, blobs */ + static constexpr const char *EXT_MNT_POINT = "/ext"; + static constexpr const char *EXT_CONTACTS_FILE = "/ext/contacts3"; + static constexpr const char *EXT_CHANNELS_FILE = "/ext/channels2"; + static constexpr const char *EXT_ADV_BLOBS_FILE = "/ext/adv_blobs"; + + /* Fallback to internal if no external */ + static constexpr const char *INT_CONTACTS_FILE = "/lfs/contacts3"; + static constexpr const char *INT_CHANNELS_FILE = "/lfs/channels2"; + static constexpr const char *INT_ADV_BLOBS_FILE = "/lfs/adv_blobs"; + + mesh::RTCClock *_clock; + bool _has_ext_fs; + + /* Get path based on external availability */ + const char *contactsFile() const { return _has_ext_fs ? EXT_CONTACTS_FILE : INT_CONTACTS_FILE; } + const char *channelsFile() const { return _has_ext_fs ? EXT_CHANNELS_FILE : INT_CHANNELS_FILE; } + const char *advBlobsFile() const { return _has_ext_fs ? EXT_ADV_BLOBS_FILE : INT_ADV_BLOBS_FILE; } + int maxBlobRecs() const { return _has_ext_fs ? 100 : 20; } + + void checkAdvBlobFile(); + void migrateToExternalFS(); + bool openRead(const char *path, uint8_t *buf, size_t buf_sz, size_t &out_len); + bool openWrite(const char *path, const uint8_t *buf, size_t len); + bool exists(const char *path); + bool removeFile(const char *path); + bool copyFile(const char *src, const char *dst); +}; diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp new file mode 100644 index 0000000..dce2d57 --- /dev/null +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -0,0 +1,923 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Zephyr GPS Manager - GNSS power management, fix acquisition, constellation config + * + * Event-driven state machine with no polling loops: + * - LoRa/BLE events trigger GPS enable/disable + * - k_work_delayable handles standby/timeout timers + * - GNSS callback fires on fix data from driver + * + * Power strategy: + * - Direct GPIO toggle via gps-enable alias (all boards) + * - T1000-E warm standby: VRTC stays powered during standby, preserving + * ephemeris/almanac/RTC in backup RAM for fast re-acquisition (3-8s vs 15-45s) + * - Full power-off only on user-disable or System OFF + */ + +#include "ZephyrGPSManager.h" + +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(zephcore_gps, CONFIG_ZEPHCORE_GPS_LOG_LEVEL); + +/* ========== GNSS Support ========== */ +#if DT_HAS_COMPAT_STATUS_OKAY(gnss_nmea_generic) || \ + DT_HAS_COMPAT_STATUS_OKAY(u_blox_m8) || \ + DT_HAS_COMPAT_STATUS_OKAY(u_blox_f9p) || \ + DT_HAS_COMPAT_STATUS_OKAY(quectel_lcx6g) || \ + DT_HAS_COMPAT_STATUS_OKAY(quectel_lc76g) || \ + DT_HAS_COMPAT_STATUS_OKAY(luatos_air530z) +#define HAS_GNSS 1 +#include +#else +#define HAS_GNSS 0 +#endif + +/* ========== GPS Module Capability Flags ========== + * GPS power management uses GPIO only (no PM_DEVICE): + * - Wio Tracker L1 (L76K): FORCE_ON pin LOW = hardware standby (~360µA, + * Vcc stays on, ephemeris/almanac/RTC preserved, hot-start 1-2s) + * - T1000-E (AG3335): GPS_EN LOW + VRTC HIGH = warm standby (ephemeris + * preserved via backup RAM, ~1-2µA VRTC current) + * - All boards: gps-enable alias → GPIO power control + * + * PM_DEVICE is intentionally NOT used — the system-managed PM subsystem + * auto-suspends devices during idle, calling modem_chat_run_script() from + * an unexpected context which can deadlock the system. */ + +/* ========== GPS State - Power Management ========== */ +#if HAS_GNSS +static struct gps_position current_pos; +static struct gnss_time current_utc; +static bool gps_enabled = false; +static bool gps_available = false; +static K_MUTEX_DEFINE(gps_mutex); +static gps_enable_callback_t gps_enable_cb = NULL; +static gps_fix_callback_t gps_fix_cb = NULL; +static gps_event_callback_t gps_event_cb = NULL; + +/* Pending GPS actions — set by work handlers (system work queue), + * consumed by gps_process_event() (main thread). + * This avoids calling blocking GNSS APIs from the system work queue, + * which deadlocks because modem_chat_run_script() blocks on a semaphore + * that's signaled from the same work queue. */ +#define GPS_ACTION_WAKE BIT(0) /* Wake from standby → start acquiring */ +#define GPS_ACTION_TIMEOUT BIT(1) /* Acquisition timeout → go to standby */ +#define GPS_ACTION_FIX_DONE BIT(2) /* Got enough good fixes → go to standby */ +static atomic_t pending_gps_actions; + +/* GPS Power Management State Machine */ +enum gps_state { + GPS_STATE_OFF, /* GPS disabled by user */ + GPS_STATE_STANDBY, /* GPS enabled but sleeping (5 min cycle) */ + GPS_STATE_ACQUIRING, /* GPS awake, waiting for fixes */ +}; + +static enum gps_state gps_current_state = GPS_STATE_OFF; +static uint8_t consecutive_good_fixes = 0; +static bool first_fix_acquired = false; /* True after first successful fix since enable */ +static bool gps_time_synced = false; /* True after GPS syncs RTC. Starts false at boot (RTC reset), + * set true after 3 good fixes, cleared when GPS disabled. */ +static int64_t last_fix_uptime_ms = 0; /* k_uptime when last validated fix was acquired */ +static int64_t standby_start_ms = 0; /* k_uptime when standby started (for next-wake calc) */ +static uint64_t standby_interval_ms = 0; /* How long standby lasts (for next-wake calc) */ + +#define GPS_GOOD_FIX_COUNT 3 /* Need 3 consecutive good fixes */ +#define GPS_MIN_SATELLITES 4 /* Minimum satellites for valid fix */ + +/* Runtime-configurable intervals, initialized from Kconfig defaults */ +static uint32_t gps_acquire_timeout_ms = CONFIG_ZEPHCORE_GPS_FIX_TIMEOUT_SEC * 1000U; +static uint32_t gps_wake_interval_ms = CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC * 1000U; + +/* Repeater mode intervals - GPS only for time sync */ +#define GPS_REPEATER_SYNC_INTERVAL_MS (48ULL * 60 * 60 * 1000) /* 48 hours */ +#define GPS_REPEATER_SYNC_TIMEOUT_MS (5 * 60 * 1000) /* 5 minutes */ + +static bool gps_repeater_mode = false; /* True = repeater (time sync only), False = companion */ + +/* Forward declarations for work handlers and state functions */ +static void gps_wake_work_fn(struct k_work *work); +static void gps_timeout_work_fn(struct k_work *work); +static void gps_go_to_standby(void); +static void gps_start_acquiring(void); + +/* Delayable work for event-driven timers (no polling!) */ +static K_WORK_DELAYABLE_DEFINE(gps_wake_work, gps_wake_work_fn); +static K_WORK_DELAYABLE_DEFINE(gps_timeout_work, gps_timeout_work_fn); + +#else +static gps_enable_callback_t gps_enable_cb = NULL; +#endif + +void gps_set_enable_callback(gps_enable_callback_t cb) +{ + gps_enable_cb = cb; +} + +void gps_set_fix_callback(gps_fix_callback_t cb) +{ +#if HAS_GNSS + gps_fix_cb = cb; +#else + ARG_UNUSED(cb); +#endif +} + +void gps_set_event_callback(gps_event_callback_t cb) +{ +#if HAS_GNSS + gps_event_cb = cb; +#else + ARG_UNUSED(cb); +#endif +} + +#if HAS_GNSS + +/* GNSS callback - called when new fix data is available */ +static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) +{ + ARG_UNUSED(dev); + + if (!gps_enabled) { + /* GPS disabled — ignore stale NMEA from hardware power-down. + * The GNSS driver fires callbacks as long as the UART has data, + * even after we drive GPS_EN LOW (module drains its buffer). */ + return; + } + + LOG_DBG("GNSS callback: fix=%d sats=%d state=%d", + data->info.fix_status, data->info.satellites_cnt, gps_current_state); + + k_mutex_lock(&gps_mutex, K_FOREVER); + + if (data->info.fix_status >= GNSS_FIX_STATUS_GNSS_FIX) { + current_pos.latitude_ndeg = data->nav_data.latitude; + current_pos.longitude_ndeg = data->nav_data.longitude; + current_pos.altitude_mm = data->nav_data.altitude; + current_pos.satellites = data->info.satellites_cnt; + current_pos.valid = true; + current_pos.timestamp_ms = k_uptime_get(); + current_utc = data->utc; + + /* Fix validation during acquisition */ + if (gps_current_state == GPS_STATE_ACQUIRING) { + if (data->info.satellites_cnt >= GPS_MIN_SATELLITES) { + consecutive_good_fixes++; + LOG_INF("GPS: Good fix %d/%d (sats=%d) lat=%lld lon=%lld", + consecutive_good_fixes, GPS_GOOD_FIX_COUNT, + data->info.satellites_cnt, + current_pos.latitude_ndeg / 1000000, + current_pos.longitude_ndeg / 1000000); + + if (consecutive_good_fixes >= GPS_GOOD_FIX_COUNT) { + LOG_INF("GPS: Got %d good fixes, updating location/time", + GPS_GOOD_FIX_COUNT); + + /* Mark first fix acquired (enables timeout for future cycles) */ + first_fix_acquired = true; + + /* Mark time as synced from GPS - blocks phone time sync */ + gps_time_synced = true; + last_fix_uptime_ms = k_uptime_get(); + + /* Cancel timeout */ + k_work_cancel_delayable(&gps_timeout_work); + + /* Notify fix callback with validated position */ + if (gps_fix_cb) { + double lat = (double)data->nav_data.latitude / 1000000000.0; + double lon = (double)data->nav_data.longitude / 1000000000.0; + k_mutex_unlock(&gps_mutex); + gps_fix_cb(lat, lon, gps_get_utc_time()); + } else { + k_mutex_unlock(&gps_mutex); + } + + /* Defer standby to main thread — we're on the system + * workqueue here (GNSS callback), can't call PM suspend + * (modem_chat_run_script deadlocks on same workqueue). */ + atomic_or(&pending_gps_actions, GPS_ACTION_FIX_DONE); + if (gps_event_cb) { + gps_event_cb(); + } + return; + } + } else { + /* Reset counter on bad fix (< 4 satellites) */ + if (consecutive_good_fixes > 0) { + LOG_DBG("GPS: Poor fix (sats=%d), resetting counter", + data->info.satellites_cnt); + } + consecutive_good_fixes = 0; + } + } + } else { + current_pos.valid = false; + /* Reset counter on no fix */ + if (gps_current_state == GPS_STATE_ACQUIRING && consecutive_good_fixes > 0) { + LOG_DBG("GPS: No fix, resetting counter"); + consecutive_good_fixes = 0; + } + } + + k_mutex_unlock(&gps_mutex); +} + +/* Register GNSS callback for all GNSS devices */ +GNSS_DATA_CALLBACK_DEFINE(NULL, gnss_data_cb); + +/* Find and initialize GNSS device */ +static const struct device *gnss_dev = NULL; + +/* Multi-constellation configuration — runs ONCE at boot. + * modem_chat_run_script() blocks on a semaphore signaled from the system + * work queue. Calling it after a GPIO power cycle can deadlock because: + * 1. The L76K needs ~300ms to boot after power restore + * 2. Meanwhile the modem_chat may be processing stale UART data + * 3. The script completion callback competes with NMEA processing + * + * Safe to call at boot because the driver init already ran and the chip + * is powered and outputting NMEA. After power cycles, the L76K retains + * constellation + fix rate settings in internal flash (PCAS commands + * persist). So we only need to configure once. */ +static bool gnss_configured = false; + +static void gnss_configure(void) +{ + if (gnss_configured || gnss_dev == NULL) { + return; + } + + /* Enable all available constellation systems for faster TTFF. + * Try GPS+GLONASS+Galileo+BeiDou first (AG3335 supports all). + * Fall back to GPS+GLONASS+BeiDou if Galileo not supported (L76KB). */ + gnss_systems_t systems = GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | + GNSS_SYSTEM_GALILEO | GNSS_SYSTEM_BEIDOU; + int ret = gnss_set_enabled_systems(gnss_dev, systems); + if (ret == -EINVAL) { + /* Some systems not supported — try without Galileo */ + systems = GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | GNSS_SYSTEM_BEIDOU; + ret = gnss_set_enabled_systems(gnss_dev, systems); + } + if (ret == 0) { + LOG_INF("GPS: Multi-constellation enabled"); + } else if (ret == -ENOSYS || ret == -ENOTSUP) { + LOG_INF("GPS: Constellation config not supported by driver"); + } else { + LOG_WRN("GPS: Failed to set constellations: %d", ret); + /* Will retry on next power-on cycle */ + return; + } + + /* Set 1Hz fix rate (explicit, don't rely on chip defaults) */ + ret = gnss_set_fix_rate(gnss_dev, 1000); + if (ret == 0) { + LOG_INF("GPS: Fix rate set to 1Hz"); + } else if (ret != -ENOSYS && ret != -ENOTSUP) { + LOG_WRN("GPS: Failed to set fix rate: %d", ret); + } + + gnss_configured = true; +} + +#endif /* HAS_GNSS - GPS power GPIO section is unconditional (needed for shutdown) */ + +/* ========== GPS Power GPIO Control ========== + * These are unconditional (not gated by HAS_GNSS) because + * gps_power_off_for_shutdown() must be available for System OFF + * even on boards without a GNSS driver. + * + * IMPORTANT: Do NOT touch GPIO during init! The GNSS driver needs the GPS + * to be powered and outputting NMEA for the modem pipe to work. + * We only configure GPIO lazily on first power-off request. + * + * Board-specific pins (defined in board overlays as gps-enable alias): + * - T1000-E: P1.11 (GPS_EN), P0.8 (GPS_VRTC_EN), P1.15 (GPS_RESET), P1.12 (GPS_SLEEP_INT) + * - Wio Tracker L1: P1.09 (GPS power, shared with luatos,air530z on-off-gpios) + */ +#if DT_NODE_EXISTS(DT_ALIAS(gps_enable)) +static const struct gpio_dt_spec gps_enable_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_enable), gpios); +#define HAS_GPS_POWER_CONTROL 1 +#else +#define HAS_GPS_POWER_CONTROL 0 +#endif + +/* T1000-E specific GPS control pins */ +#if DT_NODE_EXISTS(DT_ALIAS(gps_vrtc_enable)) +static const struct gpio_dt_spec gps_vrtc_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_vrtc_enable), gpios); +#define HAS_GPS_VRTC 1 +#else +#define HAS_GPS_VRTC 0 +#endif + +#if DT_NODE_EXISTS(DT_ALIAS(gps_reset)) +static const struct gpio_dt_spec gps_reset_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_reset), gpios); +#define HAS_GPS_RESET 1 +#else +#define HAS_GPS_RESET 0 +#endif + +#if DT_NODE_EXISTS(DT_ALIAS(gps_sleep_int)) +static const struct gpio_dt_spec gps_sleep_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_sleep_int), gpios); +#define HAS_GPS_SLEEP 1 +#else +#define HAS_GPS_SLEEP 0 +#endif + +/* T1000-E has extra GPS control pins that require a specific init sequence */ +#define HAS_T1000_GPS_CONTROL (HAS_GPS_VRTC || HAS_GPS_RESET || HAS_GPS_SLEEP) + +#if HAS_GPS_POWER_CONTROL +static bool gps_gpio_configured = false; +#endif + +/* GPS power control with warm standby support. + * @param on true = power on, false = power off + * @param keep_vrtc When powering off: true = keep VRTC alive (warm standby, + * preserves ephemeris/almanac/RTC for fast re-acquisition), + * false = full power-off (cold start on next wake). + * Only relevant on T1000-E (HAS_GPS_VRTC); ignored on other boards. */ +static void gps_power_control(bool on, bool keep_vrtc = false) +{ +#if HAS_GPS_POWER_CONTROL + /* Direct GPIO power control — works on all boards. + * We toggle the GPS power pin ourselves rather than using driver PM + * (driver PM can hang on modem_pipe_close / modem_chat_run_script). + * The GNSS driver's modem pipe stays open; NMEA data simply stops + * when power is cut and resumes when power is restored. */ + if (on) { +#if HAS_T1000_GPS_CONTROL + /* T1000-E power-on sequence (from Arduino target.cpp start_gps()) + * Must follow this exact order with delays: + * 1. GPS_EN HIGH, delay 10ms + * 2. GPS_VRTC_EN HIGH, delay 10ms (critical - RTC power) + * 3. GPS_RESET HIGH, delay 10ms, then LOW + * 4. GPS_SLEEP_INT HIGH + */ + if (gpio_is_ready_dt(&gps_enable_gpio)) { + gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_HIGH); + } + k_msleep(10); + +#if HAS_GPS_VRTC + if (gpio_is_ready_dt(&gps_vrtc_gpio)) { + gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_HIGH); + } + k_msleep(10); +#endif + +#if HAS_GPS_RESET + if (gpio_is_ready_dt(&gps_reset_gpio)) { + gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_HIGH); + k_msleep(10); + gpio_pin_set_dt(&gps_reset_gpio, 0); /* Release reset */ + } +#endif + +#if HAS_GPS_SLEEP + if (gpio_is_ready_dt(&gps_sleep_gpio)) { + gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_HIGH); + } +#endif + gps_gpio_configured = true; + LOG_INF("GPS power ON (T1000-E sequence)"); +#else + /* Simple boards - just GPS_EN */ + if (!gps_gpio_configured) { + if (gpio_is_ready_dt(&gps_enable_gpio)) { + gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_HIGH); + gps_gpio_configured = true; + LOG_INF("GPS power GPIO configured, set HIGH"); + } else { + LOG_WRN("GPS power GPIO not ready"); + return; + } + } else { + gpio_pin_set_dt(&gps_enable_gpio, 1); + LOG_INF("GPS power ON"); + } +#endif + } else { + /* Power off sequence */ +#if HAS_GPS_VRTC + if (!keep_vrtc) { + /* Full power-off: VRTC off too (cold start on next wake) */ + if (gpio_is_ready_dt(&gps_vrtc_gpio)) { + if (!gps_gpio_configured) { + gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_LOW); + } else { + gpio_pin_set_dt(&gps_vrtc_gpio, 0); + } + } + } + /* else: warm standby — VRTC stays HIGH, preserving + * ephemeris/almanac/RTC for fast re-acquisition (~1-2 µA) */ +#endif + if (gpio_is_ready_dt(&gps_enable_gpio)) { + if (!gps_gpio_configured) { + gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_LOW); + gps_gpio_configured = true; + } else { + gpio_pin_set_dt(&gps_enable_gpio, 0); + } + } +#if HAS_GPS_VRTC + LOG_INF("GPS power OFF (%s)", keep_vrtc ? + "standby — VRTC retained" : "full"); +#else + LOG_INF("GPS power OFF"); +#endif + } +#else + ARG_UNUSED(keep_vrtc); +#endif +} + +/* Drive all GPS power-enable GPIOs LOW for System OFF. + * Uses gpio_pin_configure_dt() so pins are properly set even if + * gps_power_control() was never called (GPIO not yet configured). */ +void gps_power_off_for_shutdown(void) +{ +#if HAS_GPS_POWER_CONTROL + if (gpio_is_ready_dt(&gps_enable_gpio)) { + gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_LOW); + } +#endif +#if HAS_GPS_VRTC + if (gpio_is_ready_dt(&gps_vrtc_gpio)) { + gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_LOW); + } +#endif +#if HAS_GPS_RESET + if (gpio_is_ready_dt(&gps_reset_gpio)) { + gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_LOW); + } +#endif +#if HAS_GPS_SLEEP + if (gpio_is_ready_dt(&gps_sleep_gpio)) { + gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_LOW); + } +#endif +} + +#if HAS_GNSS /* Resume GNSS-specific code */ + +/* Go to standby and schedule next wake. + * GPIO power control only — keep VRTC for warm start on T1000-E, + * FORCE_ON pin LOW for L76K hardware standby. */ +static void gps_go_to_standby(void) +{ + uint64_t wake_interval = gps_repeater_mode ? + GPS_REPEATER_SYNC_INTERVAL_MS : gps_wake_interval_ms; + + if (gps_repeater_mode) { + LOG_INF("GPS: Going to standby for 48 hours (repeater mode)"); + } else { + LOG_INF("GPS: Going to standby for %d minutes", (int)(wake_interval / 60000)); + } + gps_current_state = GPS_STATE_STANDBY; + consecutive_good_fixes = 0; + + /* Record standby timing for UI (next-wake calculation) */ + standby_start_ms = k_uptime_get(); + standby_interval_ms = wake_interval; + + /* GPIO power-off — keep VRTC for warm start on T1000-E */ + gps_power_control(false, true); + + /* NOTE: gnss_configured stays true — L76K retains PCAS settings in + * flash across power cycles. Re-running gnss_configure() after GPIO + * wake would call modem_chat_run_script() before the chip has booted, + * risking a deadlock (modem_chat blocks on system work queue). */ + + /* Schedule next wake (event-driven, no polling!) */ + k_work_schedule(&gps_wake_work, K_MSEC(wake_interval)); +} + +/* Wake GPS and start acquiring — GPIO power-on. + * Does NOT call gnss_configure() — constellation/fix-rate settings persist + * in L76K flash across power cycles. Calling modem_chat_run_script() here + * would deadlock: the chip needs ~300ms to boot after GPIO power restore, + * but modem_chat blocks the calling thread waiting for the system work + * queue which may be processing stale UART data. */ +static void gps_start_acquiring(void) +{ + LOG_INF("GPS: Waking for %s", gps_repeater_mode ? "time sync" : "position fix"); + gps_current_state = GPS_STATE_ACQUIRING; + consecutive_good_fixes = 0; + + gps_power_control(true); + + /* Start timeout timer */ + if (gps_repeater_mode) { + /* Repeater mode: always use 5 min timeout for time sync */ + k_work_schedule(&gps_timeout_work, K_MSEC(GPS_REPEATER_SYNC_TIMEOUT_MS)); + } else if (first_fix_acquired) { + /* Companion mode: use short timeout after first fix acquired */ + k_work_schedule(&gps_timeout_work, K_MSEC(gps_acquire_timeout_ms)); + } else { + LOG_INF("GPS: First fix mode - no timeout"); + } +} + +/* Work handler: wake GPS for fix. + * Runs on system work queue — MUST NOT call blocking GNSS APIs directly! + * modem_chat_run_script() blocks on a semaphore signaled from this same + * work queue, causing a deadlock. Instead, set a flag and signal the + * main thread to do the actual wake via gps_process_event(). */ +static void gps_wake_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + if (!gps_enabled || gps_current_state != GPS_STATE_STANDBY) { + return; + } + + atomic_or(&pending_gps_actions, GPS_ACTION_WAKE); + if (gps_event_cb) { + gps_event_cb(); + } +} + +/* Work handler: timeout waiting for fix. + * Runs on system work queue — MUST NOT call blocking GNSS APIs directly! + * Same deadlock risk as gps_wake_work_fn. Defer to main thread. */ +static void gps_timeout_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + if (gps_current_state != GPS_STATE_ACQUIRING) { + return; + } + + LOG_WRN("GPS: Timeout after %d/%d fixes, deferring standby to main thread", + consecutive_good_fixes, GPS_GOOD_FIX_COUNT); + + atomic_or(&pending_gps_actions, GPS_ACTION_TIMEOUT); + if (gps_event_cb) { + gps_event_cb(); + } +} + +static int gnss_init(void) +{ + /* Try to find a GNSS device - prefer chip-specific drivers + * (they support constellation config, fix rate, etc.) over + * the generic NMEA parser which is passive only. + * Power control is done lazily in gps_enable(). */ +#if DT_HAS_COMPAT_STATUS_OKAY(quectel_lc76g) + gnss_dev = DEVICE_DT_GET_ANY(quectel_lc76g); +#elif DT_HAS_COMPAT_STATUS_OKAY(luatos_air530z) + gnss_dev = DEVICE_DT_GET_ANY(luatos_air530z); +#elif DT_HAS_COMPAT_STATUS_OKAY(quectel_lcx6g) + gnss_dev = DEVICE_DT_GET_ANY(quectel_lcx6g); +#elif DT_HAS_COMPAT_STATUS_OKAY(u_blox_m8) + gnss_dev = DEVICE_DT_GET_ANY(u_blox_m8); +#elif DT_HAS_COMPAT_STATUS_OKAY(u_blox_f9p) + gnss_dev = DEVICE_DT_GET_ANY(u_blox_f9p); +#elif DT_HAS_COMPAT_STATUS_OKAY(gnss_nmea_generic) + gnss_dev = DEVICE_DT_GET_ANY(gnss_nmea_generic); +#endif + + if (gnss_dev == NULL) { + LOG_WRN("No GNSS device found in device tree"); + return -ENODEV; + } + + if (!device_is_ready(gnss_dev)) { + LOG_ERR("GNSS device %s not ready", gnss_dev->name); + return -ENODEV; + } + + LOG_INF("GNSS device %s initialized", gnss_dev->name); + gps_available = true; + return 0; +} +#endif /* HAS_GNSS */ + +/* ========== Public API ========== */ + +int gps_manager_init(void) +{ +#if HAS_GNSS + gnss_init(); + + /* Configure constellations + fix rate NOW while chip is powered + * and the modem pipe is open (driver init already ran). + * This is the ONLY safe place to call modem_chat_run_script() — + * after power cycles the chip needs ~300ms boot time and calling + * modem_chat from the main thread risks deadlock. L76K retains + * PCAS settings in flash, so one-time config at boot is enough. */ + gnss_configure(); +#endif + return 0; +} + +bool gps_is_available(void) +{ +#if HAS_GNSS + return gps_available; +#else + return false; +#endif +} + +bool gps_is_enabled(void) +{ +#if HAS_GNSS + return gps_enabled; +#else + return false; +#endif +} + +void gps_ensure_power_state(bool should_be_enabled) +{ +#if HAS_GNSS + if (!gps_available) { + return; + } + + /* At boot, GPS hardware is powered (bootloader/pull-up). + * If it should be disabled, explicitly power it off now. */ + if (!should_be_enabled) { + LOG_INF("GPS: Powering off at boot (disabled in prefs)"); + gps_power_control(false); + gps_current_state = GPS_STATE_OFF; + } +#else + ARG_UNUSED(should_be_enabled); +#endif +} + +void gps_set_repeater_mode(bool repeater) +{ +#if HAS_GNSS + if (!gps_available) { + return; + } + + gps_repeater_mode = repeater; + + if (repeater) { + LOG_INF("GPS: Repeater mode - starting initial time sync, then every 48h"); + + gps_enabled = true; /* Logically enabled */ + + /* Start acquiring immediately for initial time sync at boot. + * GPS hardware is already powered from bootloader, so we just + * start the acquisition state machine. */ + gps_start_acquiring(); + } else { + LOG_INF("GPS: Companion mode"); + } +#else + ARG_UNUSED(repeater); +#endif +} + +void gps_enable(bool enable) +{ +#if HAS_GNSS + if (!gps_available) { + LOG_WRN("GPS not available"); + return; + } + + if (enable == gps_enabled) { + return; + } + + gps_enabled = enable; + + if (enable) { + LOG_INF("GPS enabled - starting acquisition"); + + /* Start acquiring immediately (no delay for first wake) */ + gps_current_state = GPS_STATE_ACQUIRING; + consecutive_good_fixes = 0; + + /* Power on GPS - uses lazy GPIO init */ + gps_power_control(true); + + /* gnss_configure() runs once at boot (see gps_manager_init path). + * L76K retains PCAS settings in flash across power cycles. + * Do NOT call modem_chat_run_script() here — the chip needs + * ~300ms to boot after GPIO power restore and calling it + * immediately deadlocks the main thread. */ + + /* No timeout for first fix after enable - wait as long as needed */ + if (first_fix_acquired) { + k_work_schedule(&gps_timeout_work, K_MSEC(gps_acquire_timeout_ms)); + } else { + LOG_INF("GPS: First fix mode - no timeout until first successful fix"); + } + } else { + LOG_INF("GPS disabled - canceling timers and powering off"); + + /* Cancel any pending work */ + k_work_cancel_delayable(&gps_wake_work); + k_work_cancel_delayable(&gps_timeout_work); + + /* Power off GPS */ + gps_power_control(false); + gps_current_state = GPS_STATE_OFF; + consecutive_good_fixes = 0; + + /* Clear time sync flag - time will drift, allow phone sync again */ + gps_time_synced = false; + } + + /* Notify callback (for persistence in main.cpp) */ + if (gps_enable_cb) { + gps_enable_cb(enable); + } +#else + ARG_UNUSED(enable); +#endif +} + +void gps_get_position(struct gps_position *pos) +{ +#if HAS_GNSS + k_mutex_lock(&gps_mutex, K_FOREVER); + *pos = current_pos; + k_mutex_unlock(&gps_mutex); +#else + memset(pos, 0, sizeof(*pos)); +#endif +} + +uint32_t gps_get_poll_interval_sec(void) +{ +#if HAS_GNSS + return gps_wake_interval_ms / 1000U; +#else + return CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC; +#endif +} + +void gps_set_poll_interval_sec(uint32_t interval) +{ +#if HAS_GNSS + if (interval < 10) interval = 10; + if (interval > 86400) interval = 86400; + gps_wake_interval_ms = interval * 1000U; + LOG_INF("GPS poll interval set to %u seconds", interval); +#else + ARG_UNUSED(interval); +#endif +} + +int64_t gps_get_utc_time(void) +{ +#if HAS_GNSS + k_mutex_lock(&gps_mutex, K_FOREVER); + if (!current_pos.valid) { + k_mutex_unlock(&gps_mutex); + return 0; + } + + struct gnss_time t = current_utc; + k_mutex_unlock(&gps_mutex); + + int year = 2000 + t.century_year; + int days = 0; + + for (int y = 1970; y < year; y++) { + days += (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 366 : 365; + } + + static const int month_days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + for (int m = 1; m < t.month; m++) { + days += month_days[m]; + if (m == 2 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) { + days++; + } + } + + days += t.month_day - 1; + + int64_t timestamp = (int64_t)days * 86400; + timestamp += t.hour * 3600; + timestamp += t.minute * 60; + timestamp += t.millisecond / 1000; + + return timestamp; +#else + return 0; +#endif +} + +bool gps_has_time_sync(void) +{ +#if HAS_GNSS + /* Returns true if GPS has recently synced the RTC. + * Expires after 2 hours without a fix so the phone can re-sync + * (e.g. node moved indoors, GPS lost sky, RTC drifting). */ + if (!gps_time_synced) { + return false; + } + int64_t age_ms = k_uptime_get() - last_fix_uptime_ms; + + if (age_ms > (2 * 60 * 60 * 1000LL)) { + gps_time_synced = false; + return false; + } + return true; +#else + return false; +#endif +} + +bool gps_get_last_known_position(struct gps_position *pos) +{ +#if HAS_GNSS + k_mutex_lock(&gps_mutex, K_FOREVER); + if (current_pos.valid) { + *pos = current_pos; + k_mutex_unlock(&gps_mutex); + return true; + } + k_mutex_unlock(&gps_mutex); +#endif + memset(pos, 0, sizeof(*pos)); + return false; +} + +void gps_request_fresh_fix(void) +{ +#if HAS_GNSS + if (!gps_available || !gps_enabled) { + return; + } + + if (gps_current_state == GPS_STATE_STANDBY) { + LOG_INF("GPS: Fresh fix requested, waking early"); + /* Cancel scheduled wake and wake immediately */ + k_work_cancel_delayable(&gps_wake_work); + gps_start_acquiring(); + } else if (gps_current_state == GPS_STATE_ACQUIRING) { + LOG_DBG("GPS: Fresh fix requested but already acquiring"); + } +#endif +} + +void gps_get_state_info(struct gps_state_info *info) +{ + memset(info, 0, sizeof(*info)); +#if HAS_GNSS + info->state = (uint8_t)gps_current_state; + info->satellites = current_pos.satellites; + + if (last_fix_uptime_ms > 0) { + /* Seconds since last validated fix */ + info->last_fix_age_s = (uint32_t)((k_uptime_get() - last_fix_uptime_ms) / 1000); + } else { + info->last_fix_age_s = UINT32_MAX; /* No fix yet */ + } + + if (gps_current_state == GPS_STATE_STANDBY && standby_interval_ms > 0) { + int64_t wake_at = standby_start_ms + (int64_t)standby_interval_ms; + int64_t remaining = wake_at - k_uptime_get(); + + info->next_search_s = (remaining > 0) ? (uint32_t)(remaining / 1000) : 0; + } else if (gps_current_state == GPS_STATE_ACQUIRING) { + info->next_search_s = 0; /* Searching right now */ + } +#endif +} + +/* Process pending GPS state transitions — called from main thread. + * Work handlers on the system work queue set flags + signal the main + * thread via gps_event_cb(). The main thread then calls this function, + * which safely executes blocking GNSS configuration (modem_chat_run_script + * blocks on a semaphore signaled from the system work queue — calling it + * FROM the work queue deadlocks). */ +void gps_process_event(void) +{ +#if HAS_GNSS + uint32_t actions = (uint32_t)atomic_clear(&pending_gps_actions); + + if (actions == 0) { + return; + } + + /* Wake takes priority — if both wake and timeout/fix-done are pending + * (shouldn't happen, but be safe), wake wins. */ + if (actions & GPS_ACTION_WAKE) { + if (gps_enabled && gps_current_state == GPS_STATE_STANDBY) { + gps_start_acquiring(); + } + } else if (actions & (GPS_ACTION_TIMEOUT | GPS_ACTION_FIX_DONE)) { + if (gps_current_state == GPS_STATE_ACQUIRING) { + gps_go_to_standby(); + } + } +#endif +} diff --git a/zephcore/adapters/gps/ZephyrGPSManager.h b/zephcore/adapters/gps/ZephyrGPSManager.h new file mode 100644 index 0000000..2cf95c9 --- /dev/null +++ b/zephcore/adapters/gps/ZephyrGPSManager.h @@ -0,0 +1,118 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Zephyr GPS Manager - GNSS power management, fix acquisition, constellation config + * + * Event-driven state machine: OFF → ACQUIRING → STANDBY → ACQUIRING → ... + * Uses Zephyr GNSS API with chip-specific drivers (quectel,lc76g / luatos,air530z / gnss-nmea-generic) + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* GPS position data */ +struct gps_position { + int64_t latitude_ndeg; /* Latitude in nano-degrees */ + int64_t longitude_ndeg; /* Longitude in nano-degrees */ + int32_t altitude_mm; /* Altitude in millimeters */ + uint16_t satellites; /* Number of satellites */ + bool valid; /* True if fix is valid */ + int64_t timestamp_ms; /* Timestamp when fix was acquired */ +}; + +/* GPS state info for UI display */ +struct gps_state_info { + uint8_t state; /* 0=OFF, 1=STANDBY (sleeping), 2=ACQUIRING (searching) */ + uint16_t satellites; /* Current/last satellite count */ + uint32_t last_fix_age_s; /* Seconds since last validated fix (UINT32_MAX = never) */ + uint32_t next_search_s; /* Seconds until next search (0 = searching now or off) */ +}; + +/* GPS enable callback - called when GPS is enabled/disabled (for power management) */ +typedef void (*gps_enable_callback_t)(bool enabled); +void gps_set_enable_callback(gps_enable_callback_t cb); + +/* GPS fix callback - called when GPS acquires a validated fix (3 consecutive good fixes) + * Parameters: latitude (degrees), longitude (degrees), utc_time (Unix timestamp) + * Use this to update node position for mesh advertising */ +typedef void (*gps_fix_callback_t)(double lat, double lon, int64_t utc_time); +void gps_set_fix_callback(gps_fix_callback_t cb); + +/* GPS event callback - called when the GPS state machine needs the main thread + * to process a state transition (wake from standby, timeout, etc.). + * + * IMPORTANT: GNSS drivers use modem_chat_run_script() which blocks on a + * semaphore signaled from the system work queue. GPS timer work items also + * run on the system work queue → calling blocking GNSS APIs from a timer + * work handler deadlocks the system work queue. + * + * Solution: timer work handlers signal this callback, which posts an event + * to the main mesh thread. The main thread then calls gps_process_event() + * which safely executes the blocking GNSS configuration. */ +typedef void (*gps_event_callback_t)(void); +void gps_set_event_callback(gps_event_callback_t cb); + +/* Process pending GPS state transitions — MUST be called from the main + * thread (not from system work queue) because GNSS configuration uses + * blocking modem_chat calls. Called by the mesh event loop when + * MESH_EVENT_GPS_ACTION is signaled. */ +void gps_process_event(void); + +/* Initialize GPS manager (call once at boot) */ +int gps_manager_init(void); + +/* GPS functions */ +bool gps_is_available(void); +bool gps_is_enabled(void); +void gps_enable(bool enable); + +/* Ensure GPS power state matches prefs at boot. + * Call this after loading prefs - if GPS should be disabled, powers it off. + * This is needed because GPS hardware starts powered (bootloader/pull-up). */ +void gps_ensure_power_state(bool should_be_enabled); +void gps_get_position(struct gps_position *pos); +uint32_t gps_get_poll_interval_sec(void); +void gps_set_poll_interval_sec(uint32_t interval); + +/* Get RTC time from GPS (returns Unix timestamp, 0 if no valid time) */ +int64_t gps_get_utc_time(void); + +/* Check if we have GPS-synced time (used to reject phone time sync attempts) */ +bool gps_has_time_sync(void); + +/* Get last known GPS position (even when GPS is sleeping/disabled) + * Returns true if a valid last position exists, false otherwise. + * This is useful for telemetry when GPS is in power-save mode. */ +bool gps_get_last_known_position(struct gps_position *pos); + +/* Trigger GPS wake for a fresh fix (if GPS is enabled but sleeping). + * Call this after telemetry requests so next request has fresh location. + * Does nothing if GPS is disabled or already acquiring. */ +void gps_request_fresh_fix(void); + +/* GPS state info for UI display */ +void gps_get_state_info(struct gps_state_info *info); + +/* Drive all GPS power-enable GPIOs LOW for System OFF. + * Safe to call from any context — only touches GPIO, no BLE or SPI. + * Must be called before sys_poweroff() to prevent GPS module drawing + * current while nRF52840 is in System OFF (GPIO latches persist). */ +void gps_power_off_for_shutdown(void); + +/* Set repeater mode for GPS - time sync only, minimal power usage. + * In repeater mode: + * - GPS starts powered OFF + * - Wakes every 48 hours for RTC time sync + * - 5 minute timeout to acquire fix + * - Powers off after fix or timeout + * Call this at boot for repeater role before any gps_enable() calls. */ +void gps_set_repeater_mode(bool repeater); + +#ifdef __cplusplus +} +#endif diff --git a/zephcore/adapters/radio/.gitkeep b/zephcore/adapters/radio/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/zephcore/adapters/radio/.gitkeep @@ -0,0 +1 @@ + diff --git a/zephcore/adapters/radio/LR1110Radio.cpp b/zephcore/adapters/radio/LR1110Radio.cpp new file mode 100644 index 0000000..aed6005 --- /dev/null +++ b/zephcore/adapters/radio/LR1110Radio.cpp @@ -0,0 +1,98 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * LR1110 hardware hooks for LoRaRadioBase. + */ + +#include "LR1110Radio.h" +#include + +/* LR11xx driver extension API */ +extern "C" { +#include "lr11xx_lora.h" +} + +#include +LOG_MODULE_REGISTER(lr1110_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); + +namespace mesh { + +K_THREAD_STACK_DEFINE(lr11xx_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE); + +LR1110Radio::LR1110Radio(const struct device *lora_dev, MainBoard &board, + NodePrefs *prefs) + : LoRaRadioBase(lora_dev, board, prefs) +{ +} + +void LR1110Radio::begin() +{ + startTxThread(lr11xx_tx_wait_stack, + K_THREAD_STACK_SIZEOF(lr11xx_tx_wait_stack)); + LoRaRadioBase::begin(); +} + +/* ── Hardware primitives ──────────────────────────────────────────────── */ + +void LR1110Radio::hwConfigure(const struct lora_modem_config &cfg) +{ + int ret = lora_config(_dev, const_cast(&cfg)); + if (ret < 0) { + LOG_ERR("lora_config failed: %d", ret); + } +} + +void LR1110Radio::hwStartReceive() +{ + int ret = lora_recv_async(_dev, rxCallbackStatic, this); + if (ret < 0) { + LOG_ERR("lora_recv_async failed: %d", ret); + _in_recv_mode = false; + return; + } + _in_recv_mode = true; + + if (_rx_boost_enabled) { + lr11xx_set_rx_boost(_dev, true); + } + if (_rx_duty_cycle_enabled) { + lr11xx_set_rx_duty_cycle(_dev, true); + } +} + +void LR1110Radio::hwCancelReceive() +{ + lora_recv_async(_dev, NULL, NULL); +} + +int LR1110Radio::hwSendAsync(uint8_t *buf, uint32_t len, + struct k_poll_signal *sig) +{ + return lora_send_async(_dev, buf, len, sig); +} + +int16_t LR1110Radio::hwGetCurrentRSSI() +{ + return lr11xx_get_rssi_inst(_dev); +} + +bool LR1110Radio::hwIsPreambleDetected() +{ + return lr11xx_is_receiving(_dev); +} + +void LR1110Radio::hwSetRxBoost(bool enable) +{ + lr11xx_set_rx_boost(_dev, enable); +} + +void LR1110Radio::hwSetRxDutyCycle(bool enable) +{ + lr11xx_set_rx_duty_cycle(_dev, enable); +} + +void LR1110Radio::hwResetAGC() +{ + /* LR1110 doesn't have the same AGC issues as SX126x */ +} + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/LR1110Radio.h b/zephcore/adapters/radio/LR1110Radio.h new file mode 100644 index 0000000..35df561 --- /dev/null +++ b/zephcore/adapters/radio/LR1110Radio.h @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Radio adapter for LR1110/LR1120/LR1121 using Zephyr LoRa driver + * + * Thin wrapper around LoRaRadioBase — only hardware-specific hooks. + */ + +#pragma once + +#include "LoRaRadioBase.h" + +namespace mesh { + +class LR1110Radio : public LoRaRadioBase { +public: + LR1110Radio(const struct device *lora_dev, MainBoard &board, + NodePrefs *prefs = nullptr); + + void begin() override; + +protected: + /* Hardware primitives */ + void hwConfigure(const struct lora_modem_config &cfg) override; + void hwStartReceive() override; + void hwCancelReceive() override; + int hwSendAsync(uint8_t *buf, uint32_t len, + struct k_poll_signal *sig) override; + int16_t hwGetCurrentRSSI() override; + bool hwIsPreambleDetected() override; + void hwSetRxBoost(bool enable) override; + void hwSetRxDutyCycle(bool enable) override; + void hwResetAGC() override; +}; + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp new file mode 100644 index 0000000..2acfc79 --- /dev/null +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -0,0 +1,489 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * LoRa radio base class — all shared algorithms. + */ + +#include "LoRaRadioBase.h" +#include "radio_common.h" +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(lora_radio_base, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); + +namespace mesh { + +/* ── Constructor ──────────────────────────────────────────────────────── */ + +LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board, + NodePrefs *prefs) + : _dev(lora_dev), _prefs(prefs), _board(&board), + _in_recv_mode(false), _tx_active(false), + _last_rssi(0), _last_snr(0), + _rx_head(0), _rx_tail(0), + _noise_floor(DEFAULT_NOISE_FLOOR), _calibration_threshold(0), + _rx_duty_cycle_enabled(IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)), + _rx_boost_enabled(true), + _rx_cb(nullptr), _rx_cb_user_data(nullptr), + _tx_done_cb(nullptr), _tx_done_cb_user_data(nullptr), + _tx_thread_running(false), + _packets_recv(0), _packets_sent(0), _packets_recv_errors(0) +{ + k_poll_signal_init(&_tx_signal); + k_sem_init(&_tx_start_sem, 0, 1); + memset(_rx_ring, 0, sizeof(_rx_ring)); +} + +/* ── TX wait thread ───────────────────────────────────────────────────── */ + +void LoRaRadioBase::txWaitThreadFn(void *p1, void *p2, void *p3) +{ + LoRaRadioBase *self = static_cast(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + LOG_INF("TX wait thread started"); + + for (;;) { + k_sem_take(&self->_tx_start_sem, K_FOREVER); + + if (!self->_tx_active) { + continue; + } + + LOG_DBG("TX wait: waiting for signal..."); + + struct k_poll_event events[1] = { + K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SIGNAL, + K_POLL_MODE_NOTIFY_ONLY, + &self->_tx_signal), + }; + + /* Check if signal was already raised */ + unsigned int signaled; + int result; + k_poll_signal_check(&self->_tx_signal, &signaled, &result); + if (signaled) { + LOG_DBG("TX wait: signal already raised (result=%d)", result); + k_poll_signal_reset(&self->_tx_signal); + self->_board->onAfterTransmit(); + self->startReceive(); + self->_tx_active = false; + self->_packets_sent++; + if (self->_tx_done_cb) { + self->_tx_done_cb(self->_tx_done_cb_user_data); + } + continue; + } + + int ret = k_poll(events, 1, K_MSEC(TX_TIMEOUT_MS)); + if (ret == -EAGAIN) { + LOG_ERR("TX wait: TIMEOUT!"); + self->_board->onAfterTransmit(); + self->startReceive(); + self->_tx_active = false; + if (self->_tx_done_cb) { + self->_tx_done_cb(self->_tx_done_cb_user_data); + } + continue; + } + + if (ret == 0 && events[0].state == K_POLL_STATE_SIGNALED) { + k_poll_signal_reset(&self->_tx_signal); + self->_board->onAfterTransmit(); + self->startReceive(); + self->_tx_active = false; + self->_packets_sent++; + LOG_INF("TX complete, RX restarted"); + + if (self->_tx_done_cb) { + self->_tx_done_cb(self->_tx_done_cb_user_data); + } + } else { + LOG_ERR("TX wait: k_poll returned %d, state=%d — recovering", + ret, events[0].state); + k_poll_signal_reset(&self->_tx_signal); + self->_board->onAfterTransmit(); + self->startReceive(); + self->_tx_active = false; + + if (self->_tx_done_cb) { + self->_tx_done_cb(self->_tx_done_cb_user_data); + } + } + } +} + +void LoRaRadioBase::startTxThread(k_thread_stack_t *stack, size_t stack_size) +{ + if (_tx_thread_running) { + return; + } + k_thread_create(&_tx_wait_thread, stack, stack_size, + txWaitThreadFn, this, NULL, NULL, + TX_WAIT_THREAD_PRIORITY, 0, K_NO_WAIT); + k_thread_name_set(&_tx_wait_thread, "lora_tx_wait"); + _tx_thread_running = true; +} + +/* ── RX callback (static, ISR-safe) ──────────────────────────────────── */ + +void LoRaRadioBase::rxCallbackStatic(const struct device *dev, uint8_t *data, + uint16_t size, int16_t rssi, int8_t snr, + void *user_data) +{ + LoRaRadioBase *self = static_cast(user_data); + + /* NULL data = RX error (CRC/header error) */ + if (data == NULL && size == 0) { + self->_packets_recv_errors++; + LOG_DBG("RX error (CRC/header), total errors: %u", + self->_packets_recv_errors); + return; + } + + LOG_DBG("RX callback: size=%u rssi=%d snr=%d", size, rssi, snr); + + /* Ring buffer write */ + uint8_t next_head = (self->_rx_head + 1) % RX_RING_SIZE; + if (next_head == self->_rx_tail) { + LOG_WRN("RX ring full, dropping oldest packet"); + self->_rx_tail = (self->_rx_tail + 1) % RX_RING_SIZE; + } + + RxPacket *pkt = &self->_rx_ring[self->_rx_head]; + uint16_t copy_len = (size > sizeof(pkt->data)) ? sizeof(pkt->data) : size; + memcpy(pkt->data, data, copy_len); + pkt->len = copy_len; + pkt->rssi = rssi; + pkt->snr = snr; + + self->_rx_head = next_head; + self->_last_rssi = (float)rssi; + self->_last_snr = (float)snr; + self->_packets_recv++; + + if (self->_rx_cb) { + self->_rx_cb(self->_rx_cb_user_data); + } +} + +/* ── Config helpers ───────────────────────────────────────────────────── */ + +void LoRaRadioBase::buildModemConfig(struct lora_modem_config &cfg, bool tx) +{ + memset(&cfg, 0, sizeof(cfg)); + cfg.frequency = _prefs ? (uint32_t)(_prefs->freq * 1000000.0f) + : LoRaConfig::FREQ_HZ; + cfg.bandwidth = bw_khz_to_enum( + _prefs ? (uint16_t)(_prefs->bw) : (uint16_t)LoRaConfig::BANDWIDTH); + cfg.datarate = (enum lora_datarate)( + _prefs ? _prefs->sf : LoRaConfig::SPREADING_FACTOR); + cfg.coding_rate = cr_to_enum( + _prefs ? _prefs->cr : LoRaConfig::CODING_RATE); + cfg.preamble_len = LoRaConfig::PREAMBLE_LEN; + cfg.tx_power = _prefs ? (int8_t)_prefs->tx_power_dbm + : LoRaConfig::TX_POWER_DBM; + cfg.tx = tx; + cfg.iq_inverted = false; + cfg.public_network = false; + cfg.packet_crc_disable = false; +} + +void LoRaRadioBase::configureRx() +{ + struct lora_modem_config cfg; + buildModemConfig(cfg, false); + + LOG_DBG("configureRx: freq=%u bw=%d sf=%d cr=%d pwr=%d", + cfg.frequency, (int)cfg.bandwidth, (int)cfg.datarate, + (int)cfg.coding_rate, cfg.tx_power); + + hwConfigure(cfg); +} + +void LoRaRadioBase::configureTx() +{ + struct lora_modem_config cfg; + buildModemConfig(cfg, true); + hwConfigure(cfg); +} + +/* ── Lifecycle ────────────────────────────────────────────────────────── */ + +void LoRaRadioBase::begin() +{ + if (!device_is_ready(_dev)) { + LOG_ERR("LoRa device not ready"); + return; + } + + /* Subclass begin() calls startTxThread() before calling us. + * + * RX boost and duty cycle are set via constructor defaults: + * _rx_boost_enabled = true (boosted +3dB, overridable via setRxBoost()) + * _rx_duty_cycle_enabled = CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE + * Callers can override after begin() via setRxBoost() / enableRxDutyCycle(). + */ + + startReceive(); + + uint32_t freq = _prefs ? (uint32_t)(_prefs->freq * 1000000.0f) + : LoRaConfig::FREQ_HZ; + uint8_t sf = _prefs ? _prefs->sf : LoRaConfig::SPREADING_FACTOR; + uint16_t bw_khz = _prefs ? (uint16_t)(_prefs->bw) + : (uint16_t)LoRaConfig::BANDWIDTH; + uint8_t cr = _prefs ? _prefs->cr : LoRaConfig::CODING_RATE; + int8_t tx_pwr = _prefs ? (int8_t)_prefs->tx_power_dbm + : LoRaConfig::TX_POWER_DBM; + + LOG_INF("radio started: freq=%u bw=%u sf=%u cr=%u pwr=%d", + freq, bw_khz, sf, cr, tx_pwr); +} + +void LoRaRadioBase::reconfigure() +{ + hwCancelReceive(); + _in_recv_mode = false; + startReceive(); + + uint32_t freq = _prefs ? (uint32_t)(_prefs->freq * 1000000.0f) + : LoRaConfig::FREQ_HZ; + uint8_t sf = _prefs ? _prefs->sf : LoRaConfig::SPREADING_FACTOR; + uint16_t bw_khz = _prefs ? (uint16_t)(_prefs->bw) + : (uint16_t)LoRaConfig::BANDWIDTH; + uint8_t cr = _prefs ? _prefs->cr : LoRaConfig::CODING_RATE; + int8_t tx_pwr = _prefs ? (int8_t)_prefs->tx_power_dbm + : LoRaConfig::TX_POWER_DBM; + + LOG_INF("radio reconfigured: freq=%u bw=%u sf=%u cr=%u pwr=%d", + freq, bw_khz, sf, cr, tx_pwr); +} + +void LoRaRadioBase::reconfigureWithParams(float freq, float bw, uint8_t sf, uint8_t cr) +{ + if (_prefs) { + _prefs->freq = freq; + _prefs->bw = bw; + _prefs->sf = sf; + _prefs->cr = cr; + } + reconfigure(); +} + +void LoRaRadioBase::startReceive() +{ + configureRx(); + hwStartReceive(); +} + +/* ── RX/TX ────────────────────────────────────────────────────────────── */ + +int LoRaRadioBase::recvRaw(uint8_t *bytes, int sz) +{ + if (_rx_head == _rx_tail) { + return 0; + } + + RxPacket *pkt = &_rx_ring[_rx_tail]; + uint16_t len = pkt->len; + if (len > (uint16_t)sz) { + len = (uint16_t)sz; + } + + memcpy(bytes, pkt->data, len); + _last_rssi = (float)pkt->rssi; + _last_snr = (float)pkt->snr; + _rx_tail = (_rx_tail + 1) % RX_RING_SIZE; + return (int)len; +} + +bool LoRaRadioBase::startSendRaw(const uint8_t *bytes, int len) +{ + if (len > (int)sizeof(_tx_buf)) { + return false; + } + + _board->onBeforeTransmit(); + _tx_active = true; + _in_recv_mode = false; + + hwCancelReceive(); + configureTx(); + + memcpy(_tx_buf, bytes, len); + k_poll_signal_reset(&_tx_signal); + + int ret = hwSendAsync(_tx_buf, (uint32_t)len, &_tx_signal); + if (ret < 0) { + LOG_ERR("hwSendAsync failed: %d", ret); + _board->onAfterTransmit(); + _tx_active = false; + startReceive(); + return false; + } + + LOG_DBG("TX started async, len=%d", len); + k_sem_give(&_tx_start_sem); + return true; +} + +bool LoRaRadioBase::isSendComplete() +{ + return !_tx_active; +} + +void LoRaRadioBase::onSendFinished() +{ + /* Nothing needed — TX state tracked via _tx_active */ +} + +bool LoRaRadioBase::isInRecvMode() const +{ + return _in_recv_mode; +} + +float LoRaRadioBase::getLastRSSI() const +{ + return _last_rssi; +} + +float LoRaRadioBase::getLastSNR() const +{ + return _last_snr; +} + +/* ── Airtime + scoring ────────────────────────────────────────────────── */ + +uint32_t LoRaRadioBase::getEstAirtimeFor(int len_bytes) +{ + uint8_t sf = _prefs ? _prefs->sf : LoRaConfig::SPREADING_FACTOR; + float bw = _prefs ? _prefs->bw : (float)LoRaConfig::BANDWIDTH; + uint8_t cr_val = _prefs ? _prefs->cr : LoRaConfig::CODING_RATE; + + if (sf < 6) sf = 6; + if (sf > 12) sf = 12; + if (bw < 7.0f) bw = 125.0f; + if (cr_val < 5) cr_val = 5; + if (cr_val > 8) cr_val = 8; + + float t_sym = (float)(1 << sf) / (bw * 1000.0f); + float t_preamble = (LoRaConfig::PREAMBLE_LEN + 4.25f) * t_sym; + + float de = (sf >= 11) ? 1.0f : 0.0f; + float num = 8.0f * len_bytes - 4.0f * sf + 28.0f + 16.0f; + float den = 4.0f * (sf - 2.0f * de); + if (den < 1.0f) den = 4.0f; + float n_payload = 8.0f + fmaxf(ceilf(num / den) * (cr_val - 4 + 4), 0.0f); + + float t_payload = n_payload * t_sym; + return (uint32_t)((t_preamble + t_payload) * 1000.0f); +} + +float LoRaRadioBase::packetScore(float snr, int packet_len) +{ + int sf = _prefs ? _prefs->sf : LoRaConfig::SPREADING_FACTOR; + if (sf < 7 || sf > 12) return 0.0f; + if (snr < lora_snr_threshold[sf - 7]) return 0.0f; + + float success_rate = (snr - lora_snr_threshold[sf - 7]) / 10.0f; + float collision_penalty = 1.0f - ((float)packet_len / 256.0f); + float score = success_rate * collision_penalty; + if (score < 0.0f) score = 0.0f; + if (score > 1.0f) score = 1.0f; + return score; +} + +/* ── Advanced radio features ──────────────────────────────────────────── */ + +int LoRaRadioBase::getNoiseFloor() const +{ + return _noise_floor; +} + +void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold) +{ + _calibration_threshold = threshold; + + if (!_in_recv_mode || _tx_active) { + return; + } + + /* RX duty cycle: radio alternates HW RX/sleep autonomously. + * During sleep windows hwGetCurrentRSSI() returns garbage (0 or very + * high) — the sampling filter rejects those, and the count>=32 + * threshold ensures we only update when enough valid samples exist. */ + + int sum = 0; + int count = 0; + for (int i = 0; i < NUM_NOISE_FLOOR_SAMPLES; i++) { + if (isReceiving()) { + break; + } + int16_t rssi = hwGetCurrentRSSI(); + if (rssi < _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) { + sum += rssi; + count++; + } + } + + if (count >= NUM_NOISE_FLOOR_SAMPLES / 2) { + _noise_floor = sum / count; + if (_noise_floor < -120) _noise_floor = -120; + if (_noise_floor > -50) _noise_floor = -50; + LOG_DBG("noise floor: %d dBm (%d samples)", _noise_floor, count); + } +} + +void LoRaRadioBase::resetAGC() +{ + hwResetAGC(); +} + +bool LoRaRadioBase::isReceiving() +{ + if (!_in_recv_mode || _tx_active) { + return false; + } + if (hwIsPreambleDetected()) { + return true; + } + return isChannelActive(); +} + +bool LoRaRadioBase::isChannelActive(int threshold) +{ + if (threshold == 0) { + threshold = _calibration_threshold; + } + if (threshold == 0) { + return false; + } + int16_t rssi = hwGetCurrentRSSI(); + return rssi > (_noise_floor + threshold); +} + +/* ── Power saving ─────────────────────────────────────────────────────── */ + +void LoRaRadioBase::enableRxDutyCycle(bool enable) +{ + _rx_duty_cycle_enabled = enable; + LOG_INF("RX duty cycle %s", enable ? "enabled" : "disabled"); + if (_in_recv_mode) { + hwSetRxDutyCycle(enable); + } +} + +void LoRaRadioBase::setRxBoost(bool enable) +{ + _rx_boost_enabled = enable; + LOG_INF("RX boost %s (+3dB sensitivity, +2mA)", + enable ? "enabled" : "disabled"); + if (_in_recv_mode) { + hwSetRxBoost(enable); + } +} + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadioBase.h new file mode 100644 index 0000000..c9f2247 --- /dev/null +++ b/zephcore/adapters/radio/LoRaRadioBase.h @@ -0,0 +1,174 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * LoRa radio base class — all shared state and algorithms. + * + * Subclasses (SX126xRadio, LR1110Radio, …) only implement the hw*() + * primitives that talk to real hardware. Everything else lives here. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "radio_common.h" + +namespace mesh { + +class LoRaRadioBase : public Radio { +public: + LoRaRadioBase(const struct device *lora_dev, MainBoard &board, + NodePrefs *prefs = nullptr); + + void setPrefs(NodePrefs *prefs) { _prefs = prefs; } + + /* Callbacks */ + void setRxCallback(RadioRxCallback cb, void *user_data) { + _rx_cb = cb; + _rx_cb_user_data = user_data; + } + void setTxDoneCallback(RadioTxDoneCallback cb, void *user_data) { + _tx_done_cb = cb; + _tx_done_cb_user_data = user_data; + } + + /* Radio interface (all implemented in base) */ + void begin() override; + void reconfigure(); + void reconfigureWithParams(float freq, float bw, uint8_t sf, uint8_t cr); + int recvRaw(uint8_t *bytes, int sz) override; + uint32_t getEstAirtimeFor(int len_bytes) override; + float packetScore(float snr, int packet_len) override; + bool startSendRaw(const uint8_t *bytes, int len) override; + bool isSendComplete() override; + void onSendFinished() override; + bool isInRecvMode() const override; + float getLastRSSI() const override; + float getLastSNR() const override; + + /* Packet statistics */ + uint32_t getPacketsRecv() const override { return _packets_recv; } + uint32_t getPacketsSent() const override { return _packets_sent; } + uint32_t getPacketsRecvErrors() const override { return _packets_recv_errors; } + void resetStats() { + _packets_recv = 0; + _packets_sent = 0; + _packets_recv_errors = 0; + } + + /* Advanced radio features */ + int getNoiseFloor() const override; + void triggerNoiseFloorCalibrate(int threshold) override; + void resetAGC() override; + bool isReceiving() override; + + /* Extended API */ + bool isChannelActive(int threshold = 0); + + /* Power saving */ + void enableRxDutyCycle(bool enable); + bool isRxDutyCycleEnabled() const { return _rx_duty_cycle_enabled; } + void setRxBoost(bool enable); + bool isRxBoostEnabled() const { return _rx_boost_enabled; } + +protected: + /* ── Hardware primitives — subclass MUST implement ─────────── */ + + /** Apply lora_modem_config + any chip-specific extras (image cal, etc.) */ + virtual void hwConfigure(const struct lora_modem_config &cfg) = 0; + + /** Start async receive (lora_recv_async + boost/duty-cycle setup) */ + virtual void hwStartReceive() = 0; + + /** Cancel current async receive */ + virtual void hwCancelReceive() = 0; + + /** Start async send — returns 0 on success */ + virtual int hwSendAsync(uint8_t *buf, uint32_t len, + struct k_poll_signal *sig) = 0; + + /** Read instantaneous RSSI from hardware */ + virtual int16_t hwGetCurrentRSSI() = 0; + + /** Check IRQ flags for preamble/header detection */ + virtual bool hwIsPreambleDetected() = 0; + + /** Set RX LNA boost on/off */ + virtual void hwSetRxBoost(bool enable) = 0; + + /** Set RX duty cycle on/off (hardware level) */ + virtual void hwSetRxDutyCycle(bool enable) = 0; + + /** Reset AGC (chip-specific, may be no-op) */ + virtual void hwResetAGC() = 0; + + /* ── Shared helpers available to subclasses ────────────────── */ + + void buildModemConfig(struct lora_modem_config &cfg, bool tx); + void configureRx(); + void configureTx(); + void startReceive(); + + /* Subclass begin() should call this to start TX wait thread */ + void startTxThread(k_thread_stack_t *stack, size_t stack_size); + + const struct device *_dev; + NodePrefs *_prefs; + MainBoard *_board; + volatile bool _in_recv_mode; + volatile bool _tx_active; + volatile float _last_rssi; + volatile float _last_snr; + + /* RX ring buffer */ + struct RxPacket { + uint8_t data[256]; + uint16_t len; + int16_t rssi; + int8_t snr; + }; + RxPacket _rx_ring[RX_RING_SIZE]; + volatile uint8_t _rx_head; + volatile uint8_t _rx_tail; + + /* TX buffer + signal */ + uint8_t _tx_buf[256]; + struct k_poll_signal _tx_signal; + + /* Noise floor calibration state */ + int _noise_floor; + int _calibration_threshold; + + /* Power saving */ + bool _rx_duty_cycle_enabled; + bool _rx_boost_enabled; + + /* Static RX callback — passed to lora_recv_async() by subclass hwStartReceive() */ + static void rxCallbackStatic(const struct device *dev, uint8_t *data, + uint16_t size, int16_t rssi, int8_t snr, + void *user_data); + +private: + /* RX notification callback */ + RadioRxCallback _rx_cb; + void *_rx_cb_user_data; + + /* TX done callback */ + RadioTxDoneCallback _tx_done_cb; + void *_tx_done_cb_user_data; + + /* TX completion thread */ + static void txWaitThreadFn(void *p1, void *p2, void *p3); + struct k_thread _tx_wait_thread; + struct k_sem _tx_start_sem; + bool _tx_thread_running; + + /* Packet statistics */ + volatile uint32_t _packets_recv; + volatile uint32_t _packets_sent; + volatile uint32_t _packets_recv_errors; +}; + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/SX126xRadio.cpp b/zephcore/adapters/radio/SX126xRadio.cpp new file mode 100644 index 0000000..aca3a42 --- /dev/null +++ b/zephcore/adapters/radio/SX126xRadio.cpp @@ -0,0 +1,237 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SX126x hardware hooks for LoRaRadioBase. + */ + +#include "SX126xRadio.h" +#include +#include + +/* + * Access loramac-node SX126x functions for advanced features. + * These are defined in the Zephyr LoRa driver (sx126x.c/radio.c). + */ +extern "C" { +uint16_t SX126xGetIrqStatus(void); +int8_t SX126xGetRssiInst(void); +uint32_t SX126xGetRandom(void); + +#define RADIO_CALIBRATEIMAGE 0x98 +void SX126xWriteCommand(uint8_t opcode, uint8_t *buffer, uint16_t size); + +/* Radio struct for direct access to SetRxDutyCycle (SX126x only) */ +struct LoRaMacRadio_s { + void (*Init)(void *events); + int (*GetStatus)(void); + void (*SetModem)(int modem); + void (*SetChannel)(uint32_t freq); + bool (*IsChannelFree)(uint32_t freq, uint32_t rxBandwidth, int16_t rssiThresh, uint32_t maxCarrierSenseTime); + uint32_t (*Random)(void); + void (*SetRxConfig)(int modem, uint32_t bandwidth, uint32_t datarate, uint8_t coderate, + uint32_t bandwidthAfc, uint16_t preambleLen, uint16_t symbTimeout, + bool fixLen, uint8_t payloadLen, bool crcOn, bool freqHopOn, + uint8_t hopPeriod, bool iqInverted, bool rxContinuous); + void (*SetTxConfig)(int modem, int8_t power, uint32_t fdev, uint32_t bandwidth, + uint32_t datarate, uint8_t coderate, uint16_t preambleLen, + bool fixLen, bool crcOn, bool freqHopOn, uint8_t hopPeriod, + bool iqInverted, uint32_t timeout); + bool (*CheckRfFrequency)(uint32_t frequency); + uint32_t (*TimeOnAir)(int modem, uint32_t bandwidth, uint32_t datarate, uint8_t coderate, + uint16_t preambleLen, bool fixLen, uint8_t payloadLen, bool crcOn); + void (*Send)(uint8_t *buffer, uint8_t size); + void (*Sleep)(void); + void (*Standby)(void); + void (*Rx)(uint32_t timeout); + void (*StartCad)(void); + void (*SetTxContinuousWave)(uint32_t freq, int8_t power, uint16_t time); + int16_t (*Rssi)(int modem); + void (*Write)(uint32_t addr, uint8_t data); + uint8_t (*Read)(uint32_t addr); + void (*WriteBuffer)(uint32_t addr, uint8_t *buffer, uint8_t size); + void (*ReadBuffer)(uint32_t addr, uint8_t *buffer, uint8_t size); + void (*SetMaxPayloadLength)(int modem, uint8_t max); + void (*SetPublicNetwork)(bool enable); + uint32_t (*GetWakeupTime)(void); + void (*IrqProcess)(void); + void (*RxBoosted)(uint32_t timeout); + void (*SetRxDutyCycle)(uint32_t rxTime, uint32_t sleepTime); +}; +extern const struct LoRaMacRadio_s Radio __asm__("Radio"); +} + +/* IRQ status bits */ +static const uint16_t kIrqHeaderValid = 0x0010; +static const uint16_t kIrqPreambleDetected = 0x0004; + +/* RX gain register */ +#define REG_RX_GAIN 0x08AC +#define RX_GAIN_POWER_SAVE 0x94 +#define RX_GAIN_BOOSTED 0x96 + +#include +LOG_MODULE_REGISTER(zephcore_lora, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); + +namespace mesh { + +/** + * SX126x-specific: Calibrate image rejection for a minimal 4 MHz range. + */ +static void calibrateImageMinimal(uint32_t freq_hz) +{ + uint16_t freq_mhz = freq_hz / 1000000; + uint8_t cal_low = (uint8_t)(freq_mhz / 4); + uint8_t cal_high = cal_low + 1; + + uint8_t calFreq[2] = { cal_low, cal_high }; + SX126xWriteCommand(RADIO_CALIBRATEIMAGE, calFreq, 2); + + LOG_DBG("SX126x image calibration: %u-%u MHz", cal_low * 4, cal_high * 4); +} + +K_THREAD_STACK_DEFINE(sx126x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE); + +SX126xRadio::SX126xRadio(const struct device *lora_dev, MainBoard &board, + NodePrefs *prefs) + : LoRaRadioBase(lora_dev, board, prefs) +{ +} + +void SX126xRadio::begin() +{ + startTxThread(sx126x_tx_wait_stack, + K_THREAD_STACK_SIZEOF(sx126x_tx_wait_stack)); + LoRaRadioBase::begin(); +} + +/* ── Hardware primitives ──────────────────────────────────────────────── */ + +void SX126xRadio::hwConfigure(const struct lora_modem_config &cfg) +{ + int ret = lora_config(_dev, const_cast(&cfg)); + if (ret < 0) { + LOG_ERR("lora_config failed: %d", ret); + } else { + calibrateImageMinimal(cfg.frequency); + } +} + +void SX126xRadio::hwStartReceive() +{ + int ret = lora_recv_async(_dev, rxCallbackStatic, this); + if (ret < 0) { + LOG_ERR("lora_recv_async failed: %d", ret); + _in_recv_mode = false; + return; + } + _in_recv_mode = true; + + if (_rx_boost_enabled) { + ::Radio.Write(REG_RX_GAIN, RX_GAIN_BOOSTED); + } + applyRxDutyCycleIfEnabled(); +} + +void SX126xRadio::hwCancelReceive() +{ + lora_recv_async(_dev, NULL, NULL); +} + +int SX126xRadio::hwSendAsync(uint8_t *buf, uint32_t len, + struct k_poll_signal *sig) +{ + return lora_send_async(_dev, buf, len, sig); +} + +int16_t SX126xRadio::hwGetCurrentRSSI() +{ + return SX126xGetRssiInst(); +} + +bool SX126xRadio::hwIsPreambleDetected() +{ + uint16_t irq = SX126xGetIrqStatus(); + return (irq & kIrqHeaderValid) || (irq & kIrqPreambleDetected); +} + +void SX126xRadio::hwSetRxBoost(bool enable) +{ + ::Radio.Write(REG_RX_GAIN, enable ? RX_GAIN_BOOSTED : RX_GAIN_POWER_SAVE); +} + +void SX126xRadio::hwSetRxDutyCycle(bool enable) +{ + if (enable) { + applyRxDutyCycleIfEnabled(); + } else { + ::Radio.Rx(0); + if (_rx_boost_enabled) { + ::Radio.Write(REG_RX_GAIN, RX_GAIN_BOOSTED); + } + } +} + +void SX126xRadio::hwResetAGC() +{ + if (_in_recv_mode && !isReceiving()) { + hwCancelReceive(); + startReceive(); + } +} + +/* ── SX126x RX Duty Cycle — RadioLib Algorithm ───────────────────────── */ + +void SX126xRadio::applyRxDutyCycleIfEnabled() +{ + if (!_rx_duty_cycle_enabled || !_in_recv_mode) { + return; + } + + uint8_t sf = _prefs ? _prefs->sf : LoRaConfig::SPREADING_FACTOR; + float bw_khz = _prefs ? _prefs->bw : (float)LoRaConfig::BANDWIDTH; + uint16_t preamble_len = LoRaConfig::PREAMBLE_LEN; + + uint16_t min_symbols = (sf >= 7) ? RADIOLIB_MIN_SYMBOLS_SF7_PLUS + : RADIOLIB_MIN_SYMBOLS_SF6_LESS; + + int16_t sleep_symbols = (int16_t)preamble_len - (int16_t)min_symbols; + if (sleep_symbols <= 0) { + LOG_WRN("Preamble too short for duty cycle (need >%d, have %d)", + min_symbols, preamble_len); + _rx_duty_cycle_enabled = false; + return; + } + + uint32_t symbol_us = ((uint32_t)(1 << sf) * 1000) / (uint32_t)bw_khz; + uint32_t sleep_period_us = sleep_symbols * symbol_us; + + uint32_t preamble_total_us = (preamble_len + 1) * symbol_us; + int32_t wake_calc1 = ((int32_t)preamble_total_us - + ((int32_t)sleep_period_us - RADIOLIB_TCXO_DELAY_US)) / 2; + uint32_t wake_calc2 = (min_symbols + 1) * symbol_us; + + uint32_t wake_period_us = (wake_calc1 > 0 && (uint32_t)wake_calc1 > wake_calc2) + ? (uint32_t)wake_calc1 : wake_calc2; + + /* SetRxDutyCycle takes times in 15.625us steps */ + uint32_t rx_time = (wake_period_us * 64) / 1000; + uint32_t sleep_time = (sleep_period_us * 64) / 1000; + + if (rx_time < 64) rx_time = 64; + if (sleep_time < 64) sleep_time = 64; + + ::Radio.SetRxDutyCycle(rx_time, sleep_time); + + uint32_t rx_ms = (rx_time * 1000) / 64000; + uint32_t sleep_ms = (sleep_time * 1000) / 64000; + uint32_t total_ms = rx_ms + sleep_ms; + uint32_t preamble_ms = (preamble_len * symbol_us) / 1000; + + uint32_t bw_int = (uint32_t)bw_khz; + uint32_t bw_frac = (uint32_t)((bw_khz - bw_int) * 10); + LOG_INF("RX duty cycle: SF%d BW%u.%u sym=%uus preamble=%ums", + sf, bw_int, bw_frac, symbol_us, preamble_ms); + LOG_INF(" -> rx=%ums sleep=%ums period=%ums (minSym=%d sleepSym=%d)", + rx_ms, sleep_ms, total_ms, min_symbols, sleep_symbols); +} + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/SX126xRadio.h b/zephcore/adapters/radio/SX126xRadio.h new file mode 100644 index 0000000..9bd5977 --- /dev/null +++ b/zephcore/adapters/radio/SX126xRadio.h @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Radio adapter for SX126x (SX1261/SX1262/SX1268) using Zephyr LoRa driver + */ + +#pragma once + +#include "LoRaRadioBase.h" + +/* SX126x-specific: RX Duty Cycle power saving - uses RadioLib algorithm */ +#define RADIOLIB_MIN_SYMBOLS_SF7_PLUS 8 +#define RADIOLIB_MIN_SYMBOLS_SF6_LESS 12 +#define RADIOLIB_TCXO_DELAY_US 1000 /* ~1ms startup overhead */ + +namespace mesh { + +class SX126xRadio : public LoRaRadioBase { +public: + SX126xRadio(const struct device *lora_dev, MainBoard &board, + NodePrefs *prefs = nullptr); + + void begin() override; + +protected: + /* Hardware primitives */ + void hwConfigure(const struct lora_modem_config &cfg) override; + void hwStartReceive() override; + void hwCancelReceive() override; + int hwSendAsync(uint8_t *buf, uint32_t len, + struct k_poll_signal *sig) override; + int16_t hwGetCurrentRSSI() override; + bool hwIsPreambleDetected() override; + void hwSetRxBoost(bool enable) override; + void hwSetRxDutyCycle(bool enable) override; + void hwResetAGC() override; + +private: + /* SX126x-specific: RadioLib duty cycle timing algorithm */ + void applyRxDutyCycleIfEnabled(); +}; + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_hal.h b/zephcore/adapters/radio/lr11xx/lr11xx_hal.h new file mode 100644 index 0000000..0c90695 --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_hal.h @@ -0,0 +1,198 @@ +/*! + * @file lr11xx_hal.h + * + * @brief Hardware Abstraction Layer (HAL) interface for LR11XX + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LR11XX_HAL_H +#define LR11XX_HAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include +#include + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC MACROS ----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC CONSTANTS -------------------------------------------------------- + */ + +/** + * @brief Write this to SPI bus while reading data, or as a dummy/placeholder + */ +#define LR11XX_NOP ( 0x00 ) + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC TYPES ------------------------------------------------------------ + */ + +/*! + * @brief LR11XX HAL status + */ +typedef enum lr11xx_hal_status_e +{ + LR11XX_HAL_STATUS_OK = 0, + LR11XX_HAL_STATUS_ERROR = 3, +} lr11xx_hal_status_t; + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS PROTOTYPES --------------------------------------------- + */ + +/*! + * @brief Radio data transfer - write + * + * @param [in] context Radio implementation parameters + * @param [in] command Pointer to the buffer to be transmitted + * @param [in] command_length Buffer size to be transmitted + * @param [in] data Pointer to the buffer to be transmitted + * @param [in] data_length Buffer size to be transmitted + * + * @returns Operation status + */ +lr11xx_hal_status_t lr11xx_hal_write( const void* context, const uint8_t* command, const uint16_t command_length, + const uint8_t* data, const uint16_t data_length ); + +/*! + * @brief Radio data transfer - read + * + * @remark This is a two-step radio read operation. It consists of writing the command, releasing then re-asserting the + * NSS line, then reading a discarded dummy byte followed by data_length bytes of response data from the transceiver. + * While reading the dummy bytes and the response data, the implementation of this function must ensure that only zero + * bytes (NOP) are written to the SPI bus. + * + * @param [in] context Radio implementation parameters + * @param [in] command Pointer to the buffer to be transmitted + * @param [in] command_length Buffer size to be transmitted + * @param [out] data Pointer to the buffer to be received + * @param [in] data_length Buffer size to be received + * + * @returns Operation status + * + * @remark Some hardware SPI implementations write arbitary values on the MOSI line while reading. If this is done on + * the LR11XX, non-zero values may be interpreted as commands. This driver does not exploit this functionality, and + * expects that zeros be sent on the MOSI line when this command is reading the command response data. + */ +lr11xx_hal_status_t lr11xx_hal_read( const void* context, const uint8_t* command, const uint16_t command_length, + uint8_t* data, const uint16_t data_length ); + +/*! + * @brief Direct read from the SPI bus + * + * @remark Unlike @ref lr11xx_hal_read, this is a simple direct SPI bus SS/read/nSS operation. While reading the + * response data, the implementation of this function must ensure that only zero bytes (NOP) are written to the SPI bus. + * + * @remark Formerly, that function depended on a lr11xx_hal_write_read API function, which required bidirectional SPI + * communication. Given that all other radio functionality can be implemented with unidirectional SPI, it has been + * decided to make this HAL API change to simplify implementation requirements. + * + * @remark Only required by the @ref lr11xx_system_get_status and @ref lr11xx_bootloader_get_status commands + * + * @param [in] context Radio implementation parameters + * @param [out] data Pointer to the buffer to be received + * @param [in] data_length Buffer size to be received + * + * @returns Operation status + */ +lr11xx_hal_status_t lr11xx_hal_direct_read( const void* context, uint8_t* data, const uint16_t data_length ); + +/*! + * @brief Reset the radio + * + * @param [in] context Radio implementation parameters + * + * @returns Operation status + */ +lr11xx_hal_status_t lr11xx_hal_reset( const void* context ); + +/*! + * @brief Wake the radio up. + * + * @param [in] context Radio implementation parameters + * + * @returns Operation status + */ +lr11xx_hal_status_t lr11xx_hal_wakeup( const void* context ); + +/*! + * @brief Return the computed CRC + * + * @param [in] initial_value initial value of the CRC + * @param [in] buffer Buffer containing data used to compute the CRC + * @param [in] length Length of buffer + * + * @returns CRC value + */ +inline static uint8_t lr11xx_hal_compute_crc( const uint8_t initial_value, const uint8_t* buffer, uint16_t length ) +{ + uint8_t crc = initial_value; + + for( uint16_t i = 0; i < length; i++ ) + { + uint8_t extract = buffer[i]; + uint8_t sum; + + for( uint8_t j = 8; j > 0; j-- ) + { + sum = ( crc ^ extract ) & 0x01; + crc >>= 1; + + if( sum != 0 ) + { + crc ^= 0x65; + } + + extract >>= 1; + } + } + + return crc; +} + +#ifdef __cplusplus +} +#endif + +#endif // LR11XX_HAL_H diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c b/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c new file mode 100644 index 0000000..1e02782 --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c @@ -0,0 +1,344 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * LR11xx HAL implementation for Zephyr - ZephCore + * + * Based on Semtech SWDR001 lr11xx_driver and IRNAS/SWDR001-Zephyr port. + */ + +#include "lr11xx_hal_zephyr.h" +#include "lr11xx_hal.h" + +#include +LOG_MODULE_REGISTER(lr11xx_hal, CONFIG_LORA_LOG_LEVEL); + +/* Timeout for busy wait in milliseconds. + * LR1110 should respond within a few ms after most commands. + * After reset, firmware boot can take up to 273ms (datasheet). + * Use 3000ms to match RadioLib's timeout. */ +#define LR11XX_BUSY_TIMEOUT_MS 3000 + +/* Static state for DIO1 interrupt handling */ +static struct gpio_callback dio1_gpio_cb; +static lr11xx_dio1_callback_t dio1_user_cb = NULL; +static void *dio1_user_data = NULL; +static struct lr11xx_hal_context *current_ctx = NULL; + +/* Work queue for deferred DIO1 processing + * SPI operations CANNOT be done from ISR context on nRF52! + * The GPIO interrupt triggers this work item which runs in thread context. + */ +static struct k_work dio1_work; + +static void dio1_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + if (dio1_user_cb) { + dio1_user_cb(dio1_user_data); + } +} + +/** + * @brief Wait until BUSY pin goes low or timeout + */ +static lr11xx_hal_status_t wait_on_busy(struct lr11xx_hal_context *ctx) +{ + int64_t start = k_uptime_get(); + int loops = 0; + + while (gpio_pin_get_dt(&ctx->busy)) { + if ((k_uptime_get() - start) > LR11XX_BUSY_TIMEOUT_MS) { + LOG_ERR("BUSY timeout after %d loops (%dms)!", + loops, LR11XX_BUSY_TIMEOUT_MS); + return LR11XX_HAL_STATUS_ERROR; + } + k_busy_wait(100); /* 100us */ + loops++; + } + + return LR11XX_HAL_STATUS_OK; +} + +/** + * @brief Check device ready, wake from sleep if needed + */ +static lr11xx_hal_status_t check_device_ready(struct lr11xx_hal_context *ctx) +{ + if (!ctx->radio_is_sleeping) { + return wait_on_busy(ctx); + } + + /* Radio is sleeping - wake it with NSS pulse + * NSS is ACTIVE_LOW: logical 1 = physical LOW = asserted + */ + gpio_pin_set_dt(&ctx->nss, 1); /* Assert NSS (pull LOW) */ + k_busy_wait(10); + gpio_pin_set_dt(&ctx->nss, 0); /* Deassert NSS (release HIGH) */ + + ctx->radio_is_sleeping = false; + return wait_on_busy(ctx); +} + +/** + * @brief DIO1 GPIO interrupt callback (ISR context) + * + * CRITICAL: This runs in ISR context! Cannot do SPI operations here. + * Instead, we submit work to the system work queue which runs in thread context. + */ +static void dio1_isr_callback(const struct device *dev, struct gpio_callback *cb, + uint32_t pins) +{ + ARG_UNUSED(dev); + ARG_UNUSED(cb); + ARG_UNUSED(pins); + + /* Defer to work queue - SPI ops not allowed in ISR */ + k_work_submit(&dio1_work); +} + +/* Public HAL API - called by Semtech driver */ + +int lr11xx_hal_init(struct lr11xx_hal_context *ctx) +{ + int ret; + + current_ctx = ctx; + ctx->radio_is_sleeping = false; + + /* Configure NSS as output, inactive (deselected). + * GPIO_OUTPUT_INACTIVE respects GPIO_ACTIVE_LOW from DTS: + * inactive = logical 0 = physical HIGH = chip deselected. */ + ret = gpio_pin_configure_dt(&ctx->nss, GPIO_OUTPUT_INACTIVE); + if (ret < 0) { + LOG_ERR("Failed to configure NSS: %d", ret); + return ret; + } + + /* Configure RESET as output, inactive (not in reset). + * GPIO_OUTPUT_INACTIVE with GPIO_ACTIVE_LOW: + * inactive = logical 0 = physical HIGH = reset released. */ + ret = gpio_pin_configure_dt(&ctx->reset, GPIO_OUTPUT_INACTIVE); + if (ret < 0) { + LOG_ERR("Failed to configure RESET: %d", ret); + return ret; + } + + /* Configure BUSY as input */ + ret = gpio_pin_configure_dt(&ctx->busy, GPIO_INPUT); + if (ret < 0) { + LOG_ERR("Failed to configure BUSY: %d", ret); + return ret; + } + + /* Configure DIO1 as input with interrupt */ + ret = gpio_pin_configure_dt(&ctx->dio1, GPIO_INPUT); + if (ret < 0) { + LOG_ERR("Failed to configure DIO1: %d", ret); + return ret; + } + + /* Initialize DIO1 work queue handler */ + k_work_init(&dio1_work, dio1_work_handler); + + /* Set up DIO1 interrupt callback */ + gpio_init_callback(&dio1_gpio_cb, dio1_isr_callback, BIT(ctx->dio1.pin)); + ret = gpio_add_callback(ctx->dio1.port, &dio1_gpio_cb); + if (ret < 0) { + LOG_ERR("Failed to add DIO1 callback: %d", ret); + return ret; + } + + LOG_INF("LR11xx HAL initialized"); + return 0; +} + +void lr11xx_hal_set_dio1_callback(struct lr11xx_hal_context *ctx, + lr11xx_dio1_callback_t cb, void *user_data) +{ + ARG_UNUSED(ctx); + dio1_user_cb = cb; + dio1_user_data = user_data; +} + +void lr11xx_hal_enable_dio1_irq(struct lr11xx_hal_context *ctx) +{ + gpio_pin_interrupt_configure_dt(&ctx->dio1, GPIO_INT_EDGE_RISING); +} + +void lr11xx_hal_disable_dio1_irq(struct lr11xx_hal_context *ctx) +{ + gpio_pin_interrupt_configure_dt(&ctx->dio1, GPIO_INT_DISABLE); +} + +/* Semtech HAL interface implementation */ + +lr11xx_hal_status_t lr11xx_hal_write(const void *context, const uint8_t *command, + const uint16_t command_length, + const uint8_t *data, const uint16_t data_length) +{ + struct lr11xx_hal_context *ctx = (struct lr11xx_hal_context *)context; + int ret; + + if (check_device_ready(ctx) != LR11XX_HAL_STATUS_OK) { + return LR11XX_HAL_STATUS_ERROR; + } + + /* Build SPI transaction */ + const struct spi_buf tx_bufs[] = { + { .buf = (uint8_t *)command, .len = command_length }, + { .buf = (uint8_t *)data, .len = data_length }, + }; + const struct spi_buf_set tx = { + .buffers = tx_bufs, + .count = (data_length > 0) ? 2 : 1, + }; + + /* Assert NSS (active LOW in DTS, so logical 1 = physical LOW = chip selected) */ + gpio_pin_set_dt(&ctx->nss, 1); + + ret = spi_write(ctx->spi_dev, &ctx->spi_cfg, &tx); + + /* Deassert NSS (logical 0 = physical HIGH = chip deselected) */ + gpio_pin_set_dt(&ctx->nss, 0); + + if (ret < 0) { + LOG_ERR("SPI write failed: %d", ret); + return LR11XX_HAL_STATUS_ERROR; + } + + /* Check for sleep command: opcode 0x011B */ + if (command_length >= 2 && command[0] == 0x01 && command[1] == 0x1B) { + ctx->radio_is_sleeping = true; + k_busy_wait(1000); /* 1ms for sleep transition */ + return LR11XX_HAL_STATUS_OK; + } + + return wait_on_busy(ctx); +} + +lr11xx_hal_status_t lr11xx_hal_read(const void *context, const uint8_t *command, + const uint16_t command_length, + uint8_t *data, const uint16_t data_length) +{ + struct lr11xx_hal_context *ctx = (struct lr11xx_hal_context *)context; + int ret; + + /* Special case: crypto restore command needs delay */ + if (command_length >= 2 && command[0] == 0x05 && command[1] == 0x0B) { + k_busy_wait(1000); + } + + if (check_device_ready(ctx) != LR11XX_HAL_STATUS_OK) { + return LR11XX_HAL_STATUS_ERROR; + } + + /* Step 1: Write command */ + const struct spi_buf tx_buf = { .buf = (uint8_t *)command, .len = command_length }; + const struct spi_buf_set tx = { .buffers = &tx_buf, .count = 1 }; + + /* Assert NSS (active LOW in DTS, so logical 1 = physical LOW = chip selected) */ + gpio_pin_set_dt(&ctx->nss, 1); + ret = spi_write(ctx->spi_dev, &ctx->spi_cfg, &tx); + /* Deassert NSS */ + gpio_pin_set_dt(&ctx->nss, 0); + + if (ret < 0) { + LOG_ERR("SPI write (cmd) failed: %d", ret); + return LR11XX_HAL_STATUS_ERROR; + } + + if (data_length == 0) { + return wait_on_busy(ctx); + } + + /* Step 2: Wait for device ready, then read response */ + if (check_device_ready(ctx) != LR11XX_HAL_STATUS_OK) { + return LR11XX_HAL_STATUS_ERROR; + } + + /* LR11xx returns 1 dummy byte + data */ + uint8_t dummy; + const struct spi_buf rx_bufs[] = { + { .buf = &dummy, .len = 1 }, + { .buf = data, .len = data_length }, + }; + const struct spi_buf_set rx = { .buffers = rx_bufs, .count = 2 }; + + /* Assert NSS */ + gpio_pin_set_dt(&ctx->nss, 1); + ret = spi_read(ctx->spi_dev, &ctx->spi_cfg, &rx); + /* Deassert NSS */ + gpio_pin_set_dt(&ctx->nss, 0); + + if (ret < 0) { + LOG_ERR("SPI read failed: %d", ret); + return LR11XX_HAL_STATUS_ERROR; + } + + return LR11XX_HAL_STATUS_OK; +} + +lr11xx_hal_status_t lr11xx_hal_direct_read(const void *context, uint8_t *data, + const uint16_t data_length) +{ + struct lr11xx_hal_context *ctx = (struct lr11xx_hal_context *)context; + int ret; + + if (check_device_ready(ctx) != LR11XX_HAL_STATUS_OK) { + return LR11XX_HAL_STATUS_ERROR; + } + + const struct spi_buf rx_buf = { .buf = data, .len = data_length }; + const struct spi_buf_set rx = { .buffers = &rx_buf, .count = 1 }; + + /* Assert NSS (active LOW in DTS, so logical 1 = physical LOW = chip selected) */ + gpio_pin_set_dt(&ctx->nss, 1); + ret = spi_read(ctx->spi_dev, &ctx->spi_cfg, &rx); + /* Deassert NSS */ + gpio_pin_set_dt(&ctx->nss, 0); + + if (ret < 0) { + LOG_ERR("SPI direct read failed: %d", ret); + return LR11XX_HAL_STATUS_ERROR; + } + + return LR11XX_HAL_STATUS_OK; +} + +lr11xx_hal_status_t lr11xx_hal_reset(const void *context) +{ + struct lr11xx_hal_context *ctx = (struct lr11xx_hal_context *)context; + + LOG_INF("LR11xx reset: assert reset, hold 10ms"); + + /* Reset pin is ACTIVE_LOW in DTS: + * gpio_pin_set_dt(..., 1) = logical assert = physical LOW = reset active + * gpio_pin_set_dt(..., 0) = logical deassert = physical HIGH = reset released + * + * RadioLib sequence (LR_common.cpp): + * 1. digitalWrite(rst, LOW) - assert reset + * 2. delay(10) - 10ms hold + * 3. digitalWrite(rst, HIGH) - release reset + * 4. delay(300) - wait for firmware (datasheet: 273ms typical) + * 5. wait for BUSY low + */ + gpio_pin_set_dt(&ctx->reset, 1); /* Assert reset (pull LOW) */ + k_msleep(10); /* 10ms hold in reset (matches RadioLib) */ + + gpio_pin_set_dt(&ctx->reset, 0); /* Deassert reset (release to HIGH) */ + + /* Wait 300ms for internal LR11xx firmware (RadioLib uses 300ms, datasheet 273ms) */ + k_msleep(300); + + LOG_INF("LR11xx reset complete, BUSY=%d", gpio_pin_get_dt(&ctx->busy)); + + ctx->radio_is_sleeping = false; + + /* Wait for BUSY to go low - chip is ready */ + return wait_on_busy(ctx); +} + +lr11xx_hal_status_t lr11xx_hal_wakeup(const void *context) +{ + struct lr11xx_hal_context *ctx = (struct lr11xx_hal_context *)context; + return check_device_ready(ctx); +} diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.h b/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.h new file mode 100644 index 0000000..e7f4760 --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.h @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * LR11xx HAL implementation for Zephyr - ZephCore + * + * Simplified HAL that uses direct SPI/GPIO access without Zephyr device model. + * Based on IRNAS/SWDR001-Zephyr but adapted for ZephCore's simpler needs. + */ + +#ifndef LR11XX_HAL_ZEPHYR_H +#define LR11XX_HAL_ZEPHYR_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +#include "lr11xx_hal.h" + +/** + * @brief LR11xx HAL context for Zephyr + * + * This structure is passed as the 'context' parameter to all lr11xx_hal_* functions. + * It contains all the hardware configuration needed to communicate with the radio. + * + * CRITICAL: All SPI operations are protected by spi_mutex. The LR1110 radio is + * accessed from two threads: + * 1. Main thread: mesh event loop (noise floor calibration, TX, reconfigure) + * 2. System work queue: DIO1 interrupt handler (RX packet processing) + * Without the mutex, concurrent SPI access corrupts the LR1110 command/response + * protocol, causing the BUSY pin to get stuck HIGH permanently. + */ +struct lr11xx_hal_context { + /* SPI device */ + const struct device *spi_dev; + struct spi_config spi_cfg; + + /* GPIO pins */ + struct gpio_dt_spec nss; /* Chip select (directly controlled, not via SPI CS) */ + struct gpio_dt_spec reset; /* Reset pin (active low) */ + struct gpio_dt_spec busy; /* Busy pin (high = busy) */ + struct gpio_dt_spec dio1; /* DIO1 interrupt pin */ + + /* Optional pins */ + struct gpio_dt_spec dio2; /* DIO2 (often RF switch control) */ + struct gpio_dt_spec rxen; /* RX enable (for external PA/LNA) */ + struct gpio_dt_spec txen; /* TX enable (for external PA/LNA) */ + + /* TCXO config */ + uint16_t tcxo_voltage_mv; /* 0 = no TCXO, else voltage in mV (e.g., 1600 = 1.6V) */ + uint32_t tcxo_startup_us; /* TCXO startup time in microseconds */ + + /* State tracking */ + volatile bool radio_is_sleeping; +}; + +/** + * @brief Initialize the HAL context GPIOs + * + * Must be called before any other HAL functions. + * + * @param ctx HAL context with gpio specs already filled in + * @return 0 on success, negative errno on failure + */ +int lr11xx_hal_init(struct lr11xx_hal_context *ctx); + +/** + * @brief GPIO callback type for DIO1 interrupt + */ +typedef void (*lr11xx_dio1_callback_t)(void *user_data); + +/** + * @brief Set DIO1 interrupt callback + * + * @param ctx HAL context + * @param cb Callback function (called from ISR context) + * @param user_data User data passed to callback + */ +void lr11xx_hal_set_dio1_callback(struct lr11xx_hal_context *ctx, + lr11xx_dio1_callback_t cb, void *user_data); + +/** + * @brief Enable DIO1 interrupt + */ +void lr11xx_hal_enable_dio1_irq(struct lr11xx_hal_context *ctx); + +/** + * @brief Disable DIO1 interrupt + */ +void lr11xx_hal_disable_dio1_irq(struct lr11xx_hal_context *ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* LR11XX_HAL_ZEPHYR_H */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_radio.c b/zephcore/adapters/radio/lr11xx/lr11xx_radio.c new file mode 100644 index 0000000..e191510 --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_radio.c @@ -0,0 +1,1167 @@ +/*! + * @file lr11xx_radio.c + * + * @brief Radio driver implementation for LR11XX + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include "lr11xx_radio.h" +#include "lr11xx_regmem.h" +#include "lr11xx_hal.h" + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE MACROS----------------------------------------------------------- + */ + +/** + * @brief Management of the high ACP workaround + */ +#ifdef LR11XX_DISABLE_HIGH_ACP_WORKAROUND +#define LR11XX_RADIO_APPLY_HIGH_ACP_WORKAROUND( context ) LR11XX_STATUS_OK +#else +#define LR11XX_RADIO_APPLY_HIGH_ACP_WORKAROUND( context ) lr11xx_radio_apply_high_acp_workaround( context ) +#endif + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE CONSTANTS ------------------------------------------------------- + */ + +#define LR11XX_RADIO_RESET_STATS_CMD_LENGTH ( 2 ) +#define LR11XX_RADIO_GET_STATS_CMD_LENGTH ( 2 ) +#define LR11XX_RADIO_GET_PKT_TYPE_CMD_LENGTH ( 2 ) +#define LR11XX_RADIO_GET_RXBUFFER_STATUS_CMD_LENGTH ( 2 ) +#define LR11XX_RADIO_GET_PKT_STATUS_CMD_LENGTH ( 2 ) +#define LR11XX_RADIO_GET_RSSI_INST_CMD_LENGTH ( 2 ) +#define LR11XX_RADIO_SET_GFSK_SYNC_WORD_CMD_LENGTH ( 2 + LR11XX_RADIO_GFSK_SYNC_WORD_LENGTH ) +#define LR11XX_RADIO_SET_LORA_PUBLIC_NETWORK_CMD_LENGTH ( 2 + 8 ) +#define LR11XX_RADIO_SET_RX_CMD_LENGTH ( 2 + 3 ) +#define LR11XX_RADIO_SET_TX_CMD_LENGTH ( 2 + 3 ) +#define LR11XX_RADIO_SET_RF_FREQUENCY_CMD_LENGTH ( 2 + 4 ) +#define LR11XX_RADIO_SET_AUTO_TX_RX_CMD_LENGTH ( 2 + 7 ) +#define LR11XX_RADIO_SET_CAD_PARAMS_CMD_LENGTH ( 2 + 7 ) +#define LR11XX_RADIO_SET_PKT_TYPE_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_RADIO_SET_MODULATION_PARAMS_GFSK_CMD_LENGTH ( 2 + 10 ) +#define LR11XX_RADIO_SET_MODULATION_PARAMS_LORA_CMD_LENGTH ( 2 + 4 ) +#define LR11XX_RADIO_SET_PKT_PARAM_GFSK_CMD_LENGTH ( 2 + 9 ) +#define LR11XX_RADIO_SET_PKT_PARAM_LORA_CMD_LENGTH ( 2 + 6 ) +#define LR11XX_RADIO_SET_TX_PARAMS_CMD_LENGTH ( 2 + 2 ) +#define LR11XX_RADIO_SET_PKT_ADDRESS_CMD_LENGTH ( 2 + 2 ) +#define LR11XX_RADIO_SET_RX_TX_FALLBACK_MODE_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_RADIO_SET_RX_DUTY_CYCLE_MODE_CMD_LENGTH ( 2 + 7 ) +#define LR11XX_RADIO_SET_PA_CFG_CMD_LENGTH ( 2 + 4 ) +#define LR11XX_RADIO_STOP_TIMEOUT_ON_PREAMBLE_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_RADIO_SET_CAD_CMD_LENGTH ( 2 ) +#define LR11XX_RADIO_SET_TX_CW_CMD_LENGTH ( 2 ) +#define LR11XX_RADIO_SET_TX_INFINITE_PREAMBLE_CMD_LENGTH ( 2 ) +#define LR11XX_RADIO_SET_LORA_SYNC_TIMEOUT_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_RADIO_SET_GFSK_CRC_PARAMS_CMD_LENGTH ( 2 + 8 ) +#define LR11XX_RADIO_SET_GFSK_WHITENING_CMD_LENGTH ( 2 + 2 ) +#define LR11XX_RADIO_SET_RX_BOOSTED_LENGTH ( 2 + 1 ) +#define LR11XX_RADIO_SET_RSSI_CALIBRATION_LENGTH ( 2 + 11 ) +#define LR11XX_RADIO_SET_LORA_SYNC_WORD_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_RADIO_GET_LORA_RX_INFO_CMD_LENGTH ( 2 ) + +/** + * @brief Internal RTC frequency + */ +#define LR11XX_RTC_FREQ_IN_HZ 32768UL + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE TYPES ----------------------------------------------------------- + */ + +/*! + * @brief Operating codes for radio-related operations + */ +enum +{ + LR11XX_RADIO_RESET_STATS_OC = 0x0200, + LR11XX_RADIO_GET_STATS_OC = 0x0201, + LR11XX_RADIO_GET_PKT_TYPE_OC = 0x0202, + LR11XX_RADIO_GET_RXBUFFER_STATUS_OC = 0x0203, + LR11XX_RADIO_GET_PKT_STATUS_OC = 0x0204, + LR11XX_RADIO_GET_RSSI_INST_OC = 0x0205, + LR11XX_RADIO_SET_GFSK_SYNC_WORD_OC = 0x0206, + LR11XX_RADIO_SET_LORA_PUBLIC_NETWORK_OC = 0x0208, + LR11XX_RADIO_SET_RX_OC = 0x0209, + LR11XX_RADIO_SET_TX_OC = 0x020A, + LR11XX_RADIO_SET_RF_FREQUENCY_OC = 0x020B, + LR11XX_RADIO_AUTOTXRX_OC = 0x020C, + LR11XX_RADIO_SET_CAD_PARAMS_OC = 0x020D, + LR11XX_RADIO_SET_PKT_TYPE_OC = 0x020E, + LR11XX_RADIO_SET_MODULATION_PARAM_OC = 0x020F, + LR11XX_RADIO_SET_PKT_PARAM_OC = 0x0210, + LR11XX_RADIO_SET_TX_PARAMS_OC = 0x0211, + LR11XX_RADIO_SET_PKT_ADRS_OC = 0x0212, + LR11XX_RADIO_SET_RX_TX_FALLBACK_MODE_OC = 0x0213, + LR11XX_RADIO_SET_RX_DUTY_CYCLE_OC = 0x0214, + LR11XX_RADIO_SET_PA_CFG_OC = 0x0215, + LR11XX_RADIO_STOP_TIMEOUT_ON_PREAMBLE_OC = 0x0217, + LR11XX_RADIO_SET_CAD_OC = 0x0218, + LR11XX_RADIO_SET_TX_CW_OC = 0x0219, + LR11XX_RADIO_SET_TX_INFINITE_PREAMBLE_OC = 0x021A, + LR11XX_RADIO_SET_LORA_SYNC_TIMEOUT_OC = 0x021B, + LR11XX_RADIO_SET_GFSK_CRC_PARAMS_OC = 0x0224, + LR11XX_RADIO_SET_GFSK_WHITENING_PARAMS_OC = 0x0225, + LR11XX_RADIO_SET_RX_BOOSTED_OC = 0x0227, + LR11XX_RADIO_SET_RSSI_CALIBRATION_OC = 0x0229, + LR11XX_RADIO_SET_LORA_SYNC_WORD_OC = 0x022B, + LR11XX_RADIO_GET_LORA_RX_INFO_OC = 0x0230, +}; + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE VARIABLES ------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE FUNCTIONS DECLARATION ------------------------------------------- + */ + +/*! + * @brief Get the CRC length in byte from the corresponding GFSK radio parameter + * + * @param [in] crc_type GFSK CRC parameter + * + * @returns CRC length in byte + */ +static inline uint32_t lr11xx_radio_get_gfsk_crc_len_in_bytes( lr11xx_radio_gfsk_crc_type_t crc_type ); + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS DEFINITION --------------------------------------------- + */ + +lr11xx_status_t lr11xx_radio_reset_stats( const void* context ) +{ + const uint8_t cbuffer[LR11XX_RADIO_RESET_STATS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_RESET_STATS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_RESET_STATS_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_RESET_STATS_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_get_gfsk_stats( const void* context, lr11xx_radio_stats_gfsk_t* stats ) +{ + const uint8_t cbuffer[LR11XX_RADIO_GET_STATS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_GET_STATS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_GET_STATS_OC >> 0 ), + }; + uint8_t rbuffer[sizeof( lr11xx_radio_stats_gfsk_t )] = { 0x00 }; + + const lr11xx_status_t status = ( lr11xx_status_t ) lr11xx_hal_read( + context, cbuffer, LR11XX_RADIO_GET_STATS_CMD_LENGTH, rbuffer, sizeof( lr11xx_radio_stats_gfsk_t ) ); + + if( status == LR11XX_STATUS_OK ) + { + stats->nb_pkt_received = ( ( uint16_t ) rbuffer[0] << 8 ) + ( uint16_t ) rbuffer[1]; + stats->nb_pkt_crc_error = ( ( uint16_t ) rbuffer[2] << 8 ) + ( uint16_t ) rbuffer[3]; + stats->nb_pkt_len_error = ( ( uint16_t ) rbuffer[4] << 8 ) + ( uint16_t ) rbuffer[5]; + } + + return status; +} + +lr11xx_status_t lr11xx_radio_get_lora_stats( const void* context, lr11xx_radio_stats_lora_t* stats ) +{ + const uint8_t cbuffer[LR11XX_RADIO_GET_STATS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_GET_STATS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_GET_STATS_OC >> 0 ), + }; + uint8_t rbuffer[sizeof( lr11xx_radio_stats_lora_t )] = { 0x00 }; + + const lr11xx_status_t status = ( lr11xx_status_t ) lr11xx_hal_read( + context, cbuffer, LR11XX_RADIO_GET_STATS_CMD_LENGTH, rbuffer, sizeof( lr11xx_radio_stats_lora_t ) ); + + if( status == LR11XX_STATUS_OK ) + { + stats->nb_pkt_received = ( ( uint16_t ) rbuffer[0] << 8 ) + ( uint16_t ) rbuffer[1]; + stats->nb_pkt_crc_error = ( ( uint16_t ) rbuffer[2] << 8 ) + ( uint16_t ) rbuffer[3]; + stats->nb_pkt_header_error = ( ( uint16_t ) rbuffer[4] << 8 ) + ( uint16_t ) rbuffer[5]; + stats->nb_pkt_falsesync = ( ( uint16_t ) rbuffer[6] << 8 ) + ( uint16_t ) rbuffer[7]; + } + + return status; +} + +lr11xx_status_t lr11xx_radio_get_pkt_type( const void* context, lr11xx_radio_pkt_type_t* pkt_type ) +{ + const uint8_t cbuffer[LR11XX_RADIO_GET_PKT_TYPE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_GET_PKT_TYPE_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_GET_PKT_TYPE_OC >> 0 ), + }; + uint8_t pkt_type_raw = 0; + + const lr11xx_status_t status = + ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_RADIO_GET_PKT_TYPE_CMD_LENGTH, &pkt_type_raw, 1 ); + + if( status == LR11XX_STATUS_OK ) + { + *pkt_type = ( lr11xx_radio_pkt_type_t ) pkt_type_raw; + } + + return status; +} + +lr11xx_status_t lr11xx_radio_get_rx_buffer_status( const void* context, + lr11xx_radio_rx_buffer_status_t* rx_buffer_status ) +{ + const uint8_t cbuffer[LR11XX_RADIO_GET_RXBUFFER_STATUS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_GET_RXBUFFER_STATUS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_GET_RXBUFFER_STATUS_OC >> 0 ), + }; + uint8_t rbuffer[sizeof( *rx_buffer_status )] = { 0x00 }; + + const lr11xx_status_t status = ( lr11xx_status_t ) lr11xx_hal_read( + context, cbuffer, LR11XX_RADIO_GET_RXBUFFER_STATUS_CMD_LENGTH, rbuffer, sizeof( *rx_buffer_status ) ); + + if( status == LR11XX_STATUS_OK ) + { + rx_buffer_status->pld_len_in_bytes = rbuffer[0]; + rx_buffer_status->buffer_start_pointer = rbuffer[1]; + } + + return status; +} + +lr11xx_status_t lr11xx_radio_get_gfsk_pkt_status( const void* context, lr11xx_radio_pkt_status_gfsk_t* pkt_status ) +{ + const uint8_t cbuffer[LR11XX_RADIO_GET_PKT_STATUS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_GET_PKT_STATUS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_GET_PKT_STATUS_OC >> 0 ), + }; + uint8_t rbuffer[4] = { 0x00 }; + + const lr11xx_status_t status = + ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_RADIO_GET_PKT_STATUS_CMD_LENGTH, rbuffer, 4 ); + + if( status == LR11XX_STATUS_OK ) + { + pkt_status->rssi_sync_in_dbm = -( int8_t ) ( rbuffer[0] >> 1 ); + pkt_status->rssi_avg_in_dbm = -( int8_t ) ( rbuffer[1] >> 1 ); + pkt_status->rx_len_in_bytes = rbuffer[2]; + pkt_status->is_addr_err = ( ( rbuffer[3] & 0x20 ) != 0 ) ? true : false; + pkt_status->is_crc_err = ( ( rbuffer[3] & 0x10 ) != 0 ) ? true : false; + pkt_status->is_len_err = ( ( rbuffer[3] & 0x08 ) != 0 ) ? true : false; + pkt_status->is_abort_err = ( ( rbuffer[3] & 0x04 ) != 0 ) ? true : false; + pkt_status->is_received = ( ( rbuffer[3] & 0x02 ) != 0 ) ? true : false; + pkt_status->is_sent = ( ( rbuffer[3] & 0x01 ) != 0 ) ? true : false; + } + + return status; +} + +lr11xx_status_t lr11xx_radio_get_lora_pkt_status( const void* context, lr11xx_radio_pkt_status_lora_t* pkt_status ) +{ + const uint8_t cbuffer[LR11XX_RADIO_GET_PKT_STATUS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_GET_PKT_STATUS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_GET_PKT_STATUS_OC >> 0 ), + }; + uint8_t rbuffer[3] = { 0x00 }; + + const lr11xx_status_t status = + ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_RADIO_GET_PKT_STATUS_CMD_LENGTH, rbuffer, 3 ); + + if( status == LR11XX_STATUS_OK ) + { + pkt_status->rssi_pkt_in_dbm = -( int8_t ) ( rbuffer[0] >> 1 ); + pkt_status->snr_pkt_in_db = ( ( ( int8_t ) rbuffer[1] ) + 2 ) >> 2; + pkt_status->signal_rssi_pkt_in_dbm = -( int8_t ) ( rbuffer[2] >> 1 ); + } + + return status; +} + +lr11xx_status_t lr11xx_radio_get_rssi_inst( const void* context, int8_t* rssi_in_dbm ) +{ + const uint8_t cbuffer[LR11XX_RADIO_GET_RSSI_INST_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_GET_RSSI_INST_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_GET_RSSI_INST_OC >> 0 ), + }; + uint8_t rssi = 0; + + const lr11xx_status_t status = ( lr11xx_status_t ) lr11xx_hal_read( + context, cbuffer, LR11XX_RADIO_GET_RSSI_INST_CMD_LENGTH, &rssi, sizeof( rssi ) ); + + if( status == LR11XX_STATUS_OK ) + { + *rssi_in_dbm = -( int8_t ) ( rssi >> 1 ); + } + + return status; +} + +lr11xx_status_t lr11xx_radio_set_gfsk_sync_word( const void* context, + const uint8_t gfsk_sync_word[LR11XX_RADIO_GFSK_SYNC_WORD_LENGTH] ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_GFSK_SYNC_WORD_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_GFSK_SYNC_WORD_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_GFSK_SYNC_WORD_OC >> 0 ), + gfsk_sync_word[0], + gfsk_sync_word[1], + gfsk_sync_word[2], + gfsk_sync_word[3], + gfsk_sync_word[4], + gfsk_sync_word[5], + gfsk_sync_word[6], + gfsk_sync_word[7], + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_GFSK_SYNC_WORD_CMD_LENGTH, 0, 0 ); +} + +#ifndef LR11XX_DISABLE_WARNINGS +#warning \ + "The function lr11xx_radio_set_lora_sync_word replaces the \ +deprecated function lr11xx_radio_set_lora_public_network. \ +lr11xx_radio_set_lora_sync_word, however, is incompatible \ +with chip firmware versions prior to 0x303. For those legacy chips \ +only, please use lr11xx_radio_set_lora_public_network. \ +To deactivate this warning, define C preprocessor symbol \ +LR11XX_DISABLE_WARNINGS." +#endif +lr11xx_status_t lr11xx_radio_set_lora_sync_word( const void* context, const uint8_t sync_word ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_LORA_SYNC_WORD_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_LORA_SYNC_WORD_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_LORA_SYNC_WORD_OC >> 0 ), + sync_word, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_LORA_SYNC_WORD_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_lora_public_network( const void* context, + const lr11xx_radio_lora_network_type_t network_type ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_LORA_PUBLIC_NETWORK_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_LORA_PUBLIC_NETWORK_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_LORA_PUBLIC_NETWORK_OC >> 0 ), + ( uint8_t ) network_type, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_LORA_SYNC_WORD_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_rx( const void* context, const uint32_t timeout_in_ms ) +{ + const uint32_t timeout_in_rtc_step = lr11xx_radio_convert_time_in_ms_to_rtc_step( timeout_in_ms ); + + return lr11xx_radio_set_rx_with_timeout_in_rtc_step( context, timeout_in_rtc_step ); +} + +lr11xx_status_t lr11xx_radio_set_rx_with_timeout_in_rtc_step( const void* context, const uint32_t timeout ) +{ + lr11xx_status_t status = LR11XX_STATUS_ERROR; + const uint8_t cbuffer[LR11XX_RADIO_SET_RX_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_RX_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_RX_OC >> 0 ), + ( uint8_t ) ( timeout >> 16 ), + ( uint8_t ) ( timeout >> 8 ), + ( uint8_t ) ( timeout >> 0 ), + }; + + do + { + status = LR11XX_RADIO_APPLY_HIGH_ACP_WORKAROUND( context ); + if( status != LR11XX_STATUS_OK ) + { + break; + } + + status = ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_RX_CMD_LENGTH, 0, 0 ); + if( status != LR11XX_STATUS_OK ) + { + break; + } + } while( 0 ); + + return status; +} + +lr11xx_status_t lr11xx_radio_set_tx( const void* context, const uint32_t timeout_in_ms ) +{ + const uint32_t timeout_in_rtc_step = lr11xx_radio_convert_time_in_ms_to_rtc_step( timeout_in_ms ); + + return lr11xx_radio_set_tx_with_timeout_in_rtc_step( context, timeout_in_rtc_step ); +} + +lr11xx_status_t lr11xx_radio_set_tx_with_timeout_in_rtc_step( const void* context, const uint32_t timeout_in_rtc_step ) +{ + lr11xx_status_t status = LR11XX_STATUS_ERROR; + + const uint8_t cbuffer[LR11XX_RADIO_SET_TX_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_TX_OC >> 8 ), ( uint8_t ) ( LR11XX_RADIO_SET_TX_OC >> 0 ), + ( uint8_t ) ( timeout_in_rtc_step >> 16 ), ( uint8_t ) ( timeout_in_rtc_step >> 8 ), + ( uint8_t ) ( timeout_in_rtc_step >> 0 ), + }; + + do + { + status = LR11XX_RADIO_APPLY_HIGH_ACP_WORKAROUND( context ); + if( status != LR11XX_STATUS_OK ) + { + break; + } + + status = ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_TX_CMD_LENGTH, 0, 0 ); + if( status != LR11XX_STATUS_OK ) + { + break; + } + } while( 0 ); + + return status; +} + +lr11xx_status_t lr11xx_radio_set_rf_freq( const void* context, const uint32_t freq_in_hz ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_RF_FREQUENCY_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_RF_FREQUENCY_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_RF_FREQUENCY_OC >> 0 ), + ( uint8_t ) ( freq_in_hz >> 24 ), + ( uint8_t ) ( freq_in_hz >> 16 ), + ( uint8_t ) ( freq_in_hz >> 8 ), + ( uint8_t ) ( freq_in_hz >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_RF_FREQUENCY_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_auto_tx_rx( const void* context, const uint32_t delay, + const lr11xx_radio_intermediary_mode_t intermediary_mode, + const uint32_t timeout ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_AUTO_TX_RX_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_AUTOTXRX_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_AUTOTXRX_OC >> 0 ), + ( uint8_t ) ( delay >> 16 ), + ( uint8_t ) ( delay >> 8 ), + ( uint8_t ) ( delay ), + ( uint8_t ) intermediary_mode, + ( uint8_t ) ( timeout >> 16 ), + ( uint8_t ) ( timeout >> 8 ), + ( uint8_t ) ( timeout ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_AUTO_TX_RX_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_cad_params( const void* context, const lr11xx_radio_cad_params_t* cad_params ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_CAD_PARAMS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_CAD_PARAMS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_CAD_PARAMS_OC >> 0 ), + cad_params->cad_symb_nb, + cad_params->cad_detect_peak, + cad_params->cad_detect_min, + ( uint8_t ) cad_params->cad_exit_mode, + ( uint8_t ) ( cad_params->cad_timeout >> 16 ), + ( uint8_t ) ( cad_params->cad_timeout >> 8 ), + ( uint8_t ) ( cad_params->cad_timeout ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_CAD_PARAMS_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_pkt_type( const void* context, const lr11xx_radio_pkt_type_t pkt_type ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_PKT_TYPE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_PKT_TYPE_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_PKT_TYPE_OC >> 0 ), + ( uint8_t ) pkt_type, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_PKT_TYPE_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_gfsk_mod_params( const void* context, + const lr11xx_radio_mod_params_gfsk_t* mod_params ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_MODULATION_PARAMS_GFSK_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_MODULATION_PARAM_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_MODULATION_PARAM_OC >> 0 ), + ( uint8_t ) ( mod_params->br_in_bps >> 24 ), + ( uint8_t ) ( mod_params->br_in_bps >> 16 ), + ( uint8_t ) ( mod_params->br_in_bps >> 8 ), + ( uint8_t ) ( mod_params->br_in_bps >> 0 ), + ( uint8_t ) mod_params->pulse_shape, + ( uint8_t ) mod_params->bw_dsb_param, + ( uint8_t ) ( mod_params->fdev_in_hz >> 24 ), + ( uint8_t ) ( mod_params->fdev_in_hz >> 16 ), + ( uint8_t ) ( mod_params->fdev_in_hz >> 8 ), + ( uint8_t ) ( mod_params->fdev_in_hz >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_MODULATION_PARAMS_GFSK_CMD_LENGTH, + 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_lora_mod_params( const void* context, + const lr11xx_radio_mod_params_lora_t* mod_params ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_MODULATION_PARAMS_LORA_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_MODULATION_PARAM_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_MODULATION_PARAM_OC >> 0 ), + ( uint8_t ) mod_params->sf, + ( uint8_t ) mod_params->bw, + ( uint8_t ) mod_params->cr, + ( uint8_t ) mod_params->ldro, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_MODULATION_PARAMS_LORA_CMD_LENGTH, + 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_gfsk_pkt_params( const void* context, + const lr11xx_radio_pkt_params_gfsk_t* pkt_params ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_PKT_PARAM_GFSK_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_PKT_PARAM_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_PKT_PARAM_OC >> 0 ), + ( uint8_t ) ( pkt_params->preamble_len_in_bits >> 8 ), + ( uint8_t ) ( pkt_params->preamble_len_in_bits >> 0 ), + ( uint8_t ) ( pkt_params->preamble_detector ), + pkt_params->sync_word_len_in_bits, + ( uint8_t ) ( pkt_params->address_filtering ), + ( uint8_t ) ( pkt_params->header_type ), + pkt_params->pld_len_in_bytes, + ( uint8_t ) ( pkt_params->crc_type ), + ( uint8_t ) ( pkt_params->dc_free ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_PKT_PARAM_GFSK_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_lora_pkt_params( const void* context, + const lr11xx_radio_pkt_params_lora_t* pkt_params ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_PKT_PARAM_LORA_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_PKT_PARAM_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_PKT_PARAM_OC >> 0 ), + ( uint8_t ) ( pkt_params->preamble_len_in_symb >> 8 ), + ( uint8_t ) ( pkt_params->preamble_len_in_symb >> 0 ), + ( uint8_t ) ( pkt_params->header_type ), + pkt_params->pld_len_in_bytes, + ( uint8_t ) ( pkt_params->crc ), + ( uint8_t ) ( pkt_params->iq ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_PKT_PARAM_LORA_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_tx_params( const void* context, const int8_t pwr_in_dbm, + const lr11xx_radio_ramp_time_t ramp_time ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_TX_PARAMS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_TX_PARAMS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_TX_PARAMS_OC >> 0 ), + ( uint8_t ) pwr_in_dbm, + ( uint8_t ) ramp_time, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_TX_PARAMS_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_pkt_address( const void* context, const uint8_t node_address, + const uint8_t broadcast_address ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_PKT_ADDRESS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_PKT_ADRS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_PKT_ADRS_OC >> 0 ), + node_address, + broadcast_address, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_PKT_ADDRESS_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_rx_tx_fallback_mode( const void* context, + const lr11xx_radio_fallback_modes_t fallback_mode ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_RX_TX_FALLBACK_MODE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_RX_TX_FALLBACK_MODE_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_RX_TX_FALLBACK_MODE_OC >> 0 ), + fallback_mode, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_RX_TX_FALLBACK_MODE_CMD_LENGTH, 0, + 0 ); +} + +lr11xx_status_t lr11xx_radio_set_rx_duty_cycle( const void* context, const uint32_t rx_period_in_ms, + const uint32_t sleep_period_in_ms, + const lr11xx_radio_rx_duty_cycle_mode_t mode ) +{ + const uint32_t rx_period_in_rtc_step = lr11xx_radio_convert_time_in_ms_to_rtc_step( rx_period_in_ms ); + const uint32_t sleep_period_in_rtc_step = lr11xx_radio_convert_time_in_ms_to_rtc_step( sleep_period_in_ms ); + + return lr11xx_radio_set_rx_duty_cycle_with_timings_in_rtc_step( context, rx_period_in_rtc_step, + sleep_period_in_rtc_step, mode ); +} + +lr11xx_status_t lr11xx_radio_set_rx_duty_cycle_with_timings_in_rtc_step( const void* context, + const uint32_t rx_period_in_rtc_step, + const uint32_t sleep_period_in_rtc_step, + const lr11xx_radio_rx_duty_cycle_mode_t mode ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_RX_DUTY_CYCLE_MODE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_RX_DUTY_CYCLE_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_RX_DUTY_CYCLE_OC >> 0 ), + ( uint8_t ) ( rx_period_in_rtc_step >> 16 ), + ( uint8_t ) ( rx_period_in_rtc_step >> 8 ), + ( uint8_t ) ( rx_period_in_rtc_step >> 0 ), + ( uint8_t ) ( sleep_period_in_rtc_step >> 16 ), + ( uint8_t ) ( sleep_period_in_rtc_step >> 8 ), + ( uint8_t ) ( sleep_period_in_rtc_step >> 0 ), + ( uint8_t ) mode, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_RX_DUTY_CYCLE_MODE_CMD_LENGTH, 0, + 0 ); +} + +lr11xx_status_t lr11xx_radio_set_pa_cfg( const void* context, const lr11xx_radio_pa_cfg_t* pa_cfg ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_PA_CFG_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_PA_CFG_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_PA_CFG_OC >> 0 ), + ( uint8_t ) pa_cfg->pa_sel, + ( uint8_t ) pa_cfg->pa_reg_supply, + pa_cfg->pa_duty_cycle, + pa_cfg->pa_hp_sel, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_PA_CFG_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_stop_timeout_on_preamble( const void* context, const bool stop_timeout_on_preamble ) +{ + const uint8_t cbuffer[LR11XX_RADIO_STOP_TIMEOUT_ON_PREAMBLE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_STOP_TIMEOUT_ON_PREAMBLE_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_STOP_TIMEOUT_ON_PREAMBLE_OC >> 0 ), + ( uint8_t ) stop_timeout_on_preamble, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_STOP_TIMEOUT_ON_PREAMBLE_CMD_LENGTH, 0, + 0 ); +} + +lr11xx_status_t lr11xx_radio_set_cad( const void* context ) +{ + lr11xx_status_t status = LR11XX_STATUS_ERROR; + const uint8_t cbuffer[LR11XX_RADIO_SET_CAD_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_CAD_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_CAD_OC >> 0 ), + }; + + do + { + status = LR11XX_RADIO_APPLY_HIGH_ACP_WORKAROUND( context ); + if( status != LR11XX_STATUS_OK ) + { + break; + } + + status = ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_CAD_CMD_LENGTH, 0, 0 ); + if( status != LR11XX_STATUS_OK ) + { + break; + } + } while( 0 ); + + return status; +} + +lr11xx_status_t lr11xx_radio_set_tx_cw( const void* context ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_TX_CW_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_TX_CW_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_TX_CW_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_TX_CW_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_tx_infinite_preamble( const void* context ) +{ + lr11xx_status_t status = LR11XX_STATUS_ERROR; + const uint8_t cbuffer[LR11XX_RADIO_SET_TX_INFINITE_PREAMBLE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_TX_INFINITE_PREAMBLE_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_TX_INFINITE_PREAMBLE_OC >> 0 ), + }; + + do + { + status = LR11XX_RADIO_APPLY_HIGH_ACP_WORKAROUND( context ); + if( status != LR11XX_STATUS_OK ) + { + break; + } + + status = ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, + LR11XX_RADIO_SET_TX_INFINITE_PREAMBLE_CMD_LENGTH, 0, 0 ); + if( status != LR11XX_STATUS_OK ) + { + break; + } + } while( 0 ); + + return status; +} + +lr11xx_status_t lr11xx_radio_set_lora_sync_timeout( const void* context, const uint8_t nb_symbol ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_LORA_SYNC_TIMEOUT_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_LORA_SYNC_TIMEOUT_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_LORA_SYNC_TIMEOUT_OC >> 0 ), + nb_symbol, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_LORA_SYNC_TIMEOUT_CMD_LENGTH, 0, + 0 ); +} + +lr11xx_status_t lr11xx_radio_set_gfsk_crc_params( const void* context, const uint32_t seed, const uint32_t polynomial ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_GFSK_CRC_PARAMS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_GFSK_CRC_PARAMS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_GFSK_CRC_PARAMS_OC >> 0 ), + ( uint8_t ) ( seed >> 24 ), + ( uint8_t ) ( seed >> 16 ), + ( uint8_t ) ( seed >> 8 ), + ( uint8_t ) ( seed >> 0 ), + ( uint8_t ) ( polynomial >> 24 ), + ( uint8_t ) ( polynomial >> 16 ), + ( uint8_t ) ( polynomial >> 8 ), + ( uint8_t ) ( polynomial >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_GFSK_CRC_PARAMS_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_gfsk_whitening_seed( const void* context, const uint16_t seed ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_GFSK_WHITENING_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_GFSK_WHITENING_PARAMS_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_GFSK_WHITENING_PARAMS_OC >> 0 ), + ( uint8_t ) ( seed >> 8 ), + ( uint8_t ) ( seed >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_GFSK_WHITENING_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_cfg_rx_boosted( const void* context, const bool enable_boost_mode ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_RX_BOOSTED_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_RX_BOOSTED_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_RX_BOOSTED_OC >> 0 ), + ( enable_boost_mode == true ) ? 0x01 : 0x00, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_RX_BOOSTED_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_set_rssi_calibration( const void* context, + const lr11xx_radio_rssi_calibration_table_t* rssi_cal_table ) +{ + const uint8_t cbuffer[LR11XX_RADIO_SET_RSSI_CALIBRATION_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_SET_RSSI_CALIBRATION_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_SET_RSSI_CALIBRATION_OC >> 0 ), + ( uint8_t ) ( ( ( rssi_cal_table->gain_tune.g5 & 0x0F ) << 4 ) + ( rssi_cal_table->gain_tune.g4 & 0x0F ) ), + ( uint8_t ) ( ( ( rssi_cal_table->gain_tune.g7 & 0x0F ) << 4 ) + ( rssi_cal_table->gain_tune.g6 & 0x0F ) ), + ( uint8_t ) ( ( ( rssi_cal_table->gain_tune.g9 & 0x0F ) << 4 ) + ( rssi_cal_table->gain_tune.g8 & 0x0F ) ), + ( uint8_t ) ( ( ( rssi_cal_table->gain_tune.g11 & 0x0F ) << 4 ) + ( rssi_cal_table->gain_tune.g10 & 0x0F ) ), + ( uint8_t ) ( ( ( rssi_cal_table->gain_tune.g13 & 0x0F ) << 4 ) + ( rssi_cal_table->gain_tune.g12 & 0x0F ) ), + ( uint8_t ) ( ( ( rssi_cal_table->gain_tune.g13hp2 & 0x0F ) << 4 ) + + ( rssi_cal_table->gain_tune.g13hp1 & 0x0F ) ), + ( uint8_t ) ( ( ( rssi_cal_table->gain_tune.g13hp4 & 0x0F ) << 4 ) + + ( rssi_cal_table->gain_tune.g13hp3 & 0x0F ) ), + ( uint8_t ) ( ( ( rssi_cal_table->gain_tune.g13hp6 & 0x0F ) << 4 ) + + ( rssi_cal_table->gain_tune.g13hp5 & 0x0F ) ), + ( uint8_t ) ( rssi_cal_table->gain_tune.g13hp7 & 0x0F ), + ( uint8_t ) ( rssi_cal_table->gain_offset >> 8 ), + ( uint8_t ) ( rssi_cal_table->gain_offset >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_RADIO_SET_RSSI_CALIBRATION_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_radio_get_gfsk_rx_bandwidth( uint32_t bw_in_hz, lr11xx_radio_gfsk_bw_t* bw_parameter ) +{ + if( bw_in_hz <= 4800 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_4800; + } + else if( bw_in_hz <= 5800 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_5800; + } + else if( bw_in_hz <= 7300 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_7300; + } + else if( bw_in_hz <= 9700 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_9700; + } + else if( bw_in_hz <= 11700 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_11700; + } + else if( bw_in_hz <= 14600 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_14600; + } + else if( bw_in_hz <= 19500 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_19500; + } + else if( bw_in_hz <= 23400 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_23400; + } + else if( bw_in_hz <= 29300 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_29300; + } + else if( bw_in_hz <= 39000 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_39000; + } + else if( bw_in_hz <= 46900 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_46900; + } + else if( bw_in_hz <= 58600 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_58600; + } + else if( bw_in_hz <= 78200 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_78200; + } + else if( bw_in_hz <= 93800 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_93800; + } + else if( bw_in_hz <= 117300 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_117300; + } + else if( bw_in_hz <= 156200 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_156200; + } + else if( bw_in_hz <= 187200 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_187200; + } + else if( bw_in_hz <= 234300 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_234300; + } + else if( bw_in_hz <= 312000 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_312000; + } + else if( bw_in_hz <= 373600 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_373600; + } + else if( bw_in_hz <= 467000 ) + { + *bw_parameter = LR11XX_RADIO_GFSK_BW_467000; + } + else + { + return LR11XX_STATUS_ERROR; + } + + return LR11XX_STATUS_OK; +} + +uint32_t lr11xx_radio_get_lora_time_on_air_numerator( const lr11xx_radio_pkt_params_lora_t* pkt_p, + const lr11xx_radio_mod_params_lora_t* mod_p ) +{ + const int32_t pld_len_in_bytes = pkt_p->pld_len_in_bytes; + const int32_t sf = mod_p->sf; + const bool pld_is_fix = pkt_p->header_type == LR11XX_RADIO_LORA_PKT_IMPLICIT; + + int32_t fine_synch = ( sf <= 6 ) ? 1 : 0; + bool long_interleaving = ( mod_p->cr > 4 ); + + int32_t total_bytes_nb = pld_len_in_bytes + ( ( pkt_p->crc == LR11XX_RADIO_LORA_CRC_ON ) ? 2 : 0 ); + int32_t tx_bits_symbol = sf - 2 * ( mod_p->ldro != 0 ? 1 : 0 ); + + int32_t ceil_numerator; + int32_t ceil_denominator; + + uint32_t intermed; + + int32_t symbols_nb_data; + int32_t tx_infobits_header; + int32_t tx_infobits_payload; + + if( long_interleaving ) + { + const int32_t fec_rate_numerator = 4; + const int32_t fec_rate_denominator = ( mod_p->cr + ( mod_p->cr == 7 ? 1 : 0 ) ); + + if( pld_is_fix ) + { + int32_t tx_bits_symbol_start = sf - 2 + 2 * fine_synch; + if( 8 * total_bytes_nb * fec_rate_denominator <= 7 * fec_rate_numerator * tx_bits_symbol_start ) + { + ceil_numerator = 8 * total_bytes_nb * fec_rate_denominator; + ceil_denominator = fec_rate_numerator * tx_bits_symbol_start; + } + else + { + int32_t tx_codedbits_header = tx_bits_symbol_start * 8; + ceil_numerator = 8 * fec_rate_numerator * tx_bits_symbol + 8 * total_bytes_nb * fec_rate_denominator - + fec_rate_numerator * tx_codedbits_header; + ceil_denominator = fec_rate_numerator * tx_bits_symbol; + } + } + else + { + tx_infobits_header = ( sf * 4 + fine_synch * 8 - 28 ) & ~0x07; + if( tx_infobits_header < 8 * total_bytes_nb ) + { + if( tx_infobits_header > 8 * pld_len_in_bytes ) + { + tx_infobits_header = 8 * pld_len_in_bytes; + } + } + tx_infobits_payload = 8 * total_bytes_nb - tx_infobits_header; + if( tx_infobits_payload < 0 ) + { + tx_infobits_payload = 0; + } + + ceil_numerator = tx_infobits_payload * fec_rate_denominator + 8 * fec_rate_numerator * tx_bits_symbol; + ceil_denominator = fec_rate_numerator * tx_bits_symbol; + } + } + else + { + tx_infobits_header = sf * 4 + fine_synch * 8 - 8; + + if( !pld_is_fix ) + { + tx_infobits_header -= 20; + } + + tx_infobits_payload = 8 * total_bytes_nb - tx_infobits_header; + + if( tx_infobits_payload < 0 ) + tx_infobits_payload = 0; + + ceil_numerator = tx_infobits_payload; + ceil_denominator = 4 * tx_bits_symbol; + } + + symbols_nb_data = ( ( ceil_numerator + ceil_denominator - 1 ) / ceil_denominator ); + if( !long_interleaving ) + { + symbols_nb_data = symbols_nb_data * ( mod_p->cr + 4 ) + 8; + } + intermed = pkt_p->preamble_len_in_symb + 4 + 2 * fine_synch + symbols_nb_data; + + return ( uint32_t ) ( ( 4 * intermed + 1 ) * ( 1 << ( sf - 2 ) ) ) - 1; +} + +uint32_t lr11xx_radio_get_lora_bw_in_hz( lr11xx_radio_lora_bw_t bw ) +{ + uint32_t bw_in_hz = 0; + + switch( bw ) + { + case LR11XX_RADIO_LORA_BW_10: + bw_in_hz = 10417UL; + break; + case LR11XX_RADIO_LORA_BW_15: + bw_in_hz = 15625UL; + break; + case LR11XX_RADIO_LORA_BW_20: + bw_in_hz = 20833UL; + break; + case LR11XX_RADIO_LORA_BW_31: + bw_in_hz = 31250UL; + break; + case LR11XX_RADIO_LORA_BW_41: + bw_in_hz = 41667UL; + break; + case LR11XX_RADIO_LORA_BW_62: + bw_in_hz = 62500UL; + break; + case LR11XX_RADIO_LORA_BW_125: + bw_in_hz = 125000UL; + break; + case LR11XX_RADIO_LORA_BW_250: + bw_in_hz = 250000UL; + break; + case LR11XX_RADIO_LORA_BW_500: + bw_in_hz = 500000UL; + break; + case LR11XX_RADIO_LORA_BW_200: + bw_in_hz = 203000UL; + break; + case LR11XX_RADIO_LORA_BW_400: + bw_in_hz = 406000UL; + break; + case LR11XX_RADIO_LORA_BW_800: + bw_in_hz = 812000UL; + break; + } + + return bw_in_hz; +} + +uint32_t lr11xx_radio_get_lora_time_on_air_in_ms( const lr11xx_radio_pkt_params_lora_t* pkt_p, + const lr11xx_radio_mod_params_lora_t* mod_p ) +{ + uint32_t numerator = 1000U * lr11xx_radio_get_lora_time_on_air_numerator( pkt_p, mod_p ); + uint32_t denominator = lr11xx_radio_get_lora_bw_in_hz( mod_p->bw ); + // Perform integral ceil() + return ( numerator + denominator - 1 ) / denominator; +} + +uint32_t lr11xx_radio_get_gfsk_time_on_air_numerator( const lr11xx_radio_pkt_params_gfsk_t* pkt_p ) +{ + uint8_t header_len_in_bits; + + switch( pkt_p->header_type ) + { + case LR11XX_RADIO_GFSK_PKT_FIX_LEN: + { + header_len_in_bits = 0; + break; + } + case LR11XX_RADIO_GFSK_PKT_VAR_LEN: + { + header_len_in_bits = 8; + break; + } + case LR11XX_RADIO_GFSK_PKT_VAR_LEN_SX128X_COMP: + { + header_len_in_bits = 9; + break; + } + default: + { + return 0; + } + } + + return pkt_p->preamble_len_in_bits + header_len_in_bits + pkt_p->sync_word_len_in_bits + + ( ( pkt_p->pld_len_in_bytes + + ( pkt_p->address_filtering == LR11XX_RADIO_GFSK_ADDRESS_FILTERING_DISABLE ? 0 : 1 ) + + lr11xx_radio_get_gfsk_crc_len_in_bytes( pkt_p->crc_type ) ) + << 3 ); +} + +uint32_t lr11xx_radio_get_gfsk_time_on_air_in_ms( const lr11xx_radio_pkt_params_gfsk_t* pkt_p, + const lr11xx_radio_mod_params_gfsk_t* mod_p ) +{ + uint32_t numerator = 1000U * lr11xx_radio_get_gfsk_time_on_air_numerator( pkt_p ); + uint32_t denominator = mod_p->br_in_bps; + + // Perform integral ceil() + return ( numerator + denominator - 1 ) / denominator; +} + +uint32_t lr11xx_radio_convert_time_in_ms_to_rtc_step( uint32_t time_in_ms ) +{ + return ( uint32_t ) ( time_in_ms * LR11XX_RTC_FREQ_IN_HZ / 1000 ); +} + +lr11xx_status_t lr11xx_radio_get_lora_rx_info( const void* context, bool* is_crc_present, lr11xx_radio_lora_cr_t* cr ) +{ + const uint8_t cbuffer[LR11XX_RADIO_GET_LORA_RX_INFO_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_RADIO_GET_LORA_RX_INFO_OC >> 8 ), + ( uint8_t ) ( LR11XX_RADIO_GET_LORA_RX_INFO_OC >> 0 ), + }; + uint8_t rbuffer; + + const lr11xx_status_t status = + ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_RADIO_GET_LORA_RX_INFO_CMD_LENGTH, &rbuffer, 1 ); + + if( status == LR11XX_STATUS_OK ) + { + *is_crc_present = ( ( ( rbuffer & ( 0x01 << 4 ) ) != 0 ) ) ? true : false; + *cr = ( lr11xx_radio_lora_cr_t ) ( rbuffer & 0x07 ); + } + + return status; +} + +lr11xx_status_t lr11xx_radio_apply_high_acp_workaround( const void* context ) +{ + return lr11xx_regmem_write_regmem32_mask( context, 0x00F30054, 1 << 30, 0 << 30 ); +} + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE FUNCTIONS DEFINITION -------------------------------------------- + */ + +static inline uint32_t lr11xx_radio_get_gfsk_crc_len_in_bytes( lr11xx_radio_gfsk_crc_type_t crc_type ) +{ + switch( crc_type ) + { + case LR11XX_RADIO_GFSK_CRC_OFF: + return 0; + case LR11XX_RADIO_GFSK_CRC_1_BYTE: + return 1; + case LR11XX_RADIO_GFSK_CRC_2_BYTES: + return 2; + case LR11XX_RADIO_GFSK_CRC_1_BYTE_INV: + return 1; + case LR11XX_RADIO_GFSK_CRC_2_BYTES_INV: + return 2; + } + + return 0; +} + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_radio.h b/zephcore/adapters/radio/lr11xx/lr11xx_radio.h new file mode 100644 index 0000000..bcf2b0f --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_radio.h @@ -0,0 +1,776 @@ +/*! + * @file lr11xx_radio.h + * + * @brief Radio driver definition for LR11XX + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LR11XX_RADIO_H +#define LR11XX_RADIO_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include "lr11xx_radio_types.h" +#include "lr11xx_types.h" + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC MACROS ----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC CONSTANTS -------------------------------------------------------- + */ + +/*! + * @brief Length in byte of the GFSK sync word + */ +#define LR11XX_RADIO_GFSK_SYNC_WORD_LENGTH 8 + +/*! + * @brief Default GFSK sync word value + */ +#define LR11XX_RADIO_GFSK_SYNC_WORD_DEFAULT \ + { \ + 0x97, 0x23, 0x52, 0x25, 0x56, 0x53, 0x65, 0x64 \ + } + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC TYPES ------------------------------------------------------------ + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS PROTOTYPES --------------------------------------------- + */ + +/*! + * @brief Reset internal statistics of the received packets + * + * @param [in] context Chip implementation context + * + * @returns Operation status + * + * @see lr11xx_radio_get_gfsk_stats, lr11xx_radio_get_lora_stats + */ +lr11xx_status_t lr11xx_radio_reset_stats( const void* context ); + +/*! + * @brief Get the internal statistics of the GFSK received packets + * + * Internal statistics are reset on Power on Reset, by entering sleep mode without memory retention, or by calling @ref + * lr11xx_radio_reset_stats. + * + * @param [in] context Chip implementation context + * @param [out] stats The statistics structure of the received packets + * + * @returns Operation status + * + * @see lr11xx_radio_reset_stats + */ +lr11xx_status_t lr11xx_radio_get_gfsk_stats( const void* context, lr11xx_radio_stats_gfsk_t* stats ); + +/*! + * @brief Get the internal statistics of the LoRa received packets + * + * Internal statistics are reset on Power on Reset, by entering sleep mode without memory retention, or by calling @ref + * lr11xx_radio_reset_stats. + * + * @param [in] context Chip implementation context + * @param [out] stats The statistics structure of the received packets + * + * @returns Operation status + * + * @see lr11xx_radio_reset_stats + */ +lr11xx_status_t lr11xx_radio_get_lora_stats( const void* context, lr11xx_radio_stats_lora_t* stats ); + +/*! + * @brief Get the packet type currently configured + * + * @param [in] context Chip implementation context + * @param [out] pkt_type The packet type currently configured + * + * @returns Operation status + * + * @see lr11xx_radio_set_pkt_type + */ +lr11xx_status_t lr11xx_radio_get_pkt_type( const void* context, lr11xx_radio_pkt_type_t* pkt_type ); + +/*! + * @brief Get the length of last received packet, and the offset in the RX internal buffer of the first byte of the + * received payload + * + * @param [in] context Chip implementation context + * @param [out] rx_buffer_status The structure of RX buffer status + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_get_rx_buffer_status( const void* context, + lr11xx_radio_rx_buffer_status_t* rx_buffer_status ); + +/*! + * @brief Get the status of last GFSK received packet + * + * The value depends on the received packet type + * + * @param [in] context Chip implementation context + * @param [out] pkt_status The last received packet status + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_get_gfsk_pkt_status( const void* context, lr11xx_radio_pkt_status_gfsk_t* pkt_status ); + +/*! + * @brief Get the status of last LoRa received packet + * + * The value depends on the received packet type + * + * @param [in] context Chip implementation context + * @param [out] pkt_status The last received packet status + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_get_lora_pkt_status( const void* context, lr11xx_radio_pkt_status_lora_t* pkt_status ); + +/*! + * @brief Get the instantaneous RSSI. + * + * This command can be used during reception of a packet + * + * @param [in] context Chip implementation context + * @param [out] rssi_in_dbm Instantaneous RSSI. + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_get_rssi_inst( const void* context, int8_t* rssi_in_dbm ); + +/*! + * @brief Set the GFSK modem sync word + * + * This command is used to set the GFSK nodem sync word. This command expects a 8-byte long array to be passed as sync + * word parameter. By default, the value is 0x9723522556536564. + * + * @param [in] context Chip implementation context + * @param [in] gfsk_sync_word The sync word to be configured + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_gfsk_sync_word( const void* context, + const uint8_t gfsk_sync_word[LR11XX_RADIO_GFSK_SYNC_WORD_LENGTH] ); + +/*! + * @brief Set the LoRa modem sync word + * + * @param [in] context Chip implementation context + * @param [in] sync_word The sync word to be configured + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_lora_sync_word( const void* context, const uint8_t sync_word ); + +/*! + * @brief Set the LoRa modem sync word to private / public + * + * This command is used to select which LoRa network is selected + * + * @param [in] context Chip implementation context + * @param [in] network_type The network type to be configured + * + * @returns Operation status + * + * @warning This function is deprecated. Use lr11xx_radio_set_lora_sync_word for chip firmware equal to or more recent + * than 0x303. + */ +lr11xx_status_t lr11xx_radio_set_lora_public_network( const void* context, + const lr11xx_radio_lora_network_type_t network_type ); + +/*! + * @brief Start RX operations with a timeout in millisecond + * + * This command sets the LR11XX to RX mode. The radio must have been configured before using this command with @ref + * lr11xx_radio_set_pkt_type + * + * By default, the timeout parameter allows to return automatically to standby RC mode if no packets have been received + * after a certain amount of time. This behavior can be altered by @ref lr11xx_radio_set_rx_tx_fallback_mode and @ref + * lr11xx_radio_auto_tx_rx. + * + * @remark To set the radio in Rx continuous mode, the function @ref lr11xx_radio_set_rx_with_timeout_in_rtc_step has to + * be called with \p timeout_in_rtc_step set to 0xFFFFFF + * + * @param [in] context Chip implementation context + * @param [in] timeout_in_ms The timeout configuration for RX operation + * + * @returns Operation status + * + * @see lr11xx_radio_set_pkt_type, lr11xx_radio_set_rx_tx_fallback_mode + */ +lr11xx_status_t lr11xx_radio_set_rx( const void* context, const uint32_t timeout_in_ms ); + +/*! + * @brief Start RX operations with a timeout in RTC step + * + * This command sets the LR11XX to RX mode. The radio must have been configured before using this command with @ref + * lr11xx_radio_set_pkt_type + * + * By default, the timeout parameter allows to return automatically to standby RC mode if no packets have been received + * after a certain amount of time. This behavior can be altered by @ref lr11xx_radio_set_rx_tx_fallback_mode and @ref + * lr11xx_radio_auto_tx_rx. + * + * The timeout duration is obtained by: + * \f$ timeout\_duration\_ms = timeout \times \frac{1}{32.768} \f$ + * + * Maximal timeout value is 0xFFFFFF, which gives a maximal timeout of 511 seconds. + * + * The timeout argument can also have the following special values: + * + * + *
Special values Meaning
0x000000 RX single: LR11XX stays in RX mode until a + * packet is received, then switch to standby RC mode
0xFFFFFF + * RX continuous: LR11XX stays in RX mode even after reception of a + * packet + *
+ * + * @param [in] context Chip implementation context + * @param [in] timeout_in_rtc_step The timeout configuration for RX operation + * + * @returns Operation status + * + * @see lr11xx_radio_set_pkt_type, lr11xx_radio_set_rx_tx_fallback_mode + */ +lr11xx_status_t lr11xx_radio_set_rx_with_timeout_in_rtc_step( const void* context, const uint32_t timeout_in_rtc_step ); + +/*! + * @brief Start TX operations + * + * This command sets the LR11XX to TX mode. The radio must have been configured before using this command with @ref + * lr11xx_radio_set_pkt_type + * + * By default, the timeout parameter allows to return automatically to standby RC mode if the packet has not been + * completely transmitted after a certain amount of time. This behavior can be altered by @ref + * lr11xx_radio_set_rx_tx_fallback_mode and @ref lr11xx_radio_auto_tx_rx. + * + * @param [in] context Chip implementation context + * @param [in] timeout_in_ms The timeout configuration for TX operation + * + * @returns Operation status + * + * @see lr11xx_radio_set_pkt_type, lr11xx_radio_set_rx_tx_fallback_mode + */ +lr11xx_status_t lr11xx_radio_set_tx( const void* context, const uint32_t timeout_in_ms ); + +/*! + * @brief Start TX operations + * + * This command sets the LR11XX to TX mode. The radio must have been configured before using this command with @ref + * lr11xx_radio_set_pkt_type + * + * By default, the timeout parameter allows to return automatically to standby RC mode if the packet has not been + * completely transmitted after a certain amount of time. This behavior can be altered by @ref + * lr11xx_radio_set_rx_tx_fallback_mode and @ref lr11xx_radio_auto_tx_rx. + * + * The timeout duration is obtained by: + * \f$ timeout\_duration\_ms = timeout \times \frac{1}{32.768} \f$ + * + * Maximal value is 0xFFFFFF. + * + * If the timeout argument is 0, then no timeout is used. + * + * @param [in] context Chip implementation context + * @param [in] timeout_in_rtc_step The timeout configuration for TX operation + * + * @returns Operation status + * + * @see lr11xx_radio_set_pkt_type, lr11xx_radio_set_rx_tx_fallback_mode + */ +lr11xx_status_t lr11xx_radio_set_tx_with_timeout_in_rtc_step( const void* context, const uint32_t timeout_in_rtc_step ); + +/*! + * @brief Set the frequency for future radio operations. + * + * This commands does not set frequency for Wi-Fi and GNSS scan operations. + * + * @param [in] context Chip implementation context + * @param [in] freq_in_hz The frequency in Hz to set for radio operations + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_rf_freq( const void* context, const uint32_t freq_in_hz ); + +/*! + * @brief Configure automatic TX after RX or automatic RX after TX + * + * After issuing this command, using the command @ref SetTx will make the LR11XX doing the following: + * - Enter TX mode as usual + * - Enter configurable Intermediary mode during configurable delay + * - Enter RX mode + * + * Similarly, after a @ref SetRx command, the LR11XX will do the following: + * - Enter RX mode as usual + * - Enter configurable Intermediary mode during configurable delay + * - Enter TX mode + * + * In case delay is 0, the LR11XX does not enter Intermediary mode and directly enter the following mode. + * + * To disable this behavior, use this function with delay set to 0xFFFFFFFF. + * + * @param [in] context Chip implementation context + * @param [in] delay Time to spend in Intermediary mode expressed as steps of \f$\frac{1}{32.768 KHz}\f$ steps. + * @param [in] intermediary_mode The mode the LR11XX enters after first mode completion during delay time + * @param [in] timeout The timeout duration of the automatic RX or TX, expressed as steps of \f$ \frac{1}{32.768KHz} \f$ + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_auto_tx_rx( const void* context, const uint32_t delay, + const lr11xx_radio_intermediary_mode_t intermediary_mode, + const uint32_t timeout ); + +/*! + * @brief Set Channel Activity Detection configuration + * + * @param [in] context Chip implementation context + * @param [in] cad_params The structure defining CAD configuration + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_cad_params( const void* context, const lr11xx_radio_cad_params_t* cad_params ); + +/*! + * @brief Set the packet type + * + * @param [in] context Chip implementation context + * @param [in] pkt_type Packet type to set + * + * @returns Operation status + * + * @see lr11xx_radio_get_pkt_type + */ +lr11xx_status_t lr11xx_radio_set_pkt_type( const void* context, const lr11xx_radio_pkt_type_t pkt_type ); + +/*! + * @brief Set the modulation parameters for GFSK packets + * + * The command @ref lr11xx_radio_set_pkt_type must be called prior this one. + * + * @param [in] context Chip implementation context + * @param [in] mod_params The structure of modulation configuration + * + * @returns Operation status + * + * @see lr11xx_radio_set_pkt_type + */ +lr11xx_status_t lr11xx_radio_set_gfsk_mod_params( const void* context, + const lr11xx_radio_mod_params_gfsk_t* mod_params ); + +/*! + * @brief Set the modulation parameters for LoRa packets + * + * The command @ref lr11xx_radio_set_pkt_type must be called prior this one. + * + * @param [in] context Chip implementation context + * @param [in] mod_params The structure of modulation configuration + * + * @returns Operation status + * + * @see lr11xx_radio_set_pkt_type + */ +lr11xx_status_t lr11xx_radio_set_lora_mod_params( const void* context, + const lr11xx_radio_mod_params_lora_t* mod_params ); + +/*! + * @brief Set the packet parameters for GFSK packets + * + * The command @ref lr11xx_radio_set_pkt_type must be called prior this one. + * + * @param [in] context Chip implementation context + * @param [in] pkt_params The structure of packet configuration + * + * @returns Operation status + * + * @see lr11xx_radio_set_pkt_type, lr11xx_radio_set_gfsk_mod_params + */ +lr11xx_status_t lr11xx_radio_set_gfsk_pkt_params( const void* context, + const lr11xx_radio_pkt_params_gfsk_t* pkt_params ); + +/*! + * @brief Set the packet parameters for LoRa packets + * + * The command @ref lr11xx_radio_set_pkt_type must be called prior this one. + * + * @param [in] context Chip implementation context + * @param [in] pkt_params The structure of packet configuration + * + * @returns Operation status + * + * @see lr11xx_radio_set_pkt_type, lr11xx_radio_set_lora_mod_params + */ +lr11xx_status_t lr11xx_radio_set_lora_pkt_params( const void* context, + const lr11xx_radio_pkt_params_lora_t* pkt_params ); + +/*! + * @brief Set the parameters for TX power and power amplifier ramp time + * + * The command @ref lr11xx_radio_set_pa_cfg must be called prior calling + * lr11xx_radio_set_tx_params. + * + * The range of possible TX output power values depends on PA selected with @ref lr11xx_radio_set_pa_cfg : + * - for LPA: power value goes from -17dBm to +14dBm (ie. from 0xEF to 0x0E) + * - for HPA: power value goes from -9dBm to +22dBm (ie. from 0xF7 to 0x16) + * + * Moreover, to use TX output power value higher than +10dBm, the @ref REGPASUPPLY_VBAT supply must have been selected + * with @ref lr11xx_radio_set_pa_cfg. + * + * @param [in] context Chip implementation context + * @param [in] pwr_in_dbm The TX output power in dBm + * @param [in] ramp_time The ramping time configuration for the PA + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_tx_params( const void* context, const int8_t pwr_in_dbm, + const lr11xx_radio_ramp_time_t ramp_time ); + +/*! + * @brief Sets the Node and Broadcast address used for GFSK + * + * This setting is used only when filtering is enabled. + * + * @param [in] context Chip implementation context + * @param [in] node_address The node address used as filter + * @param [in] broadcast_address The broadcast address used as filter + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_pkt_address( const void* context, const uint8_t node_address, + const uint8_t broadcast_address ); + +/*! + * @brief Alter the chip mode after successfull transmission or reception operation + * + * This setting is not used during Rx Duty Cycle mode or Auto Tx Rx. + * + * @param [in] context Chip implementation context + * @param [in] fallback_mode The chip mode to enter after successfull transmission or reception. + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_rx_tx_fallback_mode( const void* context, + const lr11xx_radio_fallback_modes_t fallback_mode ); + +/*! + * @brief Configure and start a Rx Duty Cycle operation + * + * It executes the following steps: + * 1. Reception: enters reception state for duration defined by rx_period + * - If mode is LR11XX_RADIO_RX_DUTY_CYCLE_MODE_RX: it is standard RX mode + * - If mode is LR11XX_RADIO_RX_DUTY_CYCLE_MODE_CAD (only in LoRa) : it is CAD operation + * 2. Depending on the over-the-air activity detection: + * - In case of positive over-the-air detection, the rx_period timeout is recomputed to the value + * \f$2 \times rx\_period + sleep\_period\f$ + * - If no air activity is detected, the LR11XX goes back to sleep mode with retention for a duration defined by + * sleep_period + * 3. On wake-up, the LR11XX restarts the process with the reception state. + * + * @remark If mode is configured to @ref LR11XX_RADIO_RX_DUTY_CYCLE_MODE_CAD, then the CAD configuration used in step 1. + * is the one set from the last call to @ref lr11xx_radio_set_cad_params. + * + * @param [in] context Chip implementation context + * @param [in] rx_period_in_ms The length of Rx period + * @param [in] sleep_period_in_ms The length of sleep period + * @param [in] mode The operation mode during Rx phase + * + * @returns Operation status + * + * @see lr11xx_radio_set_cad_params + */ +lr11xx_status_t lr11xx_radio_set_rx_duty_cycle( const void* context, const uint32_t rx_period_in_ms, + const uint32_t sleep_period_in_ms, + const lr11xx_radio_rx_duty_cycle_mode_t mode ); + +/*! + * @brief Configure and start a Rx Duty Cycle operation + * + * It executes the following steps: + * 1. Reception: enters reception state for duration defined by rx_period + * - If mode is LR11XX_RADIO_RX_DUTY_CYCLE_MODE_RX: it is standard RX mode + * - If mode is LR11XX_RADIO_RX_DUTY_CYCLE_MODE_CAD (only in LoRa) : it is CAD operation + * 2. Depending on the over-the-air activity detection: + * - In case of positive over-the-air detection, the rx_period timeout is recomputed to the value + * \f$2 \times rx\_period + sleep\_period\f$ + * - If no air activity is detected, the LR11XX goes back to sleep mode with retention for a duration defined by + * sleep_period + * 3. On wake-up, the LR11XX restarts the process with the reception state. + * + * @remark If mode is configured to @ref LR11XX_RADIO_RX_DUTY_CYCLE_MODE_CAD, then the CAD configuration used in step 1. + * is the one set from the last call to @ref lr11xx_radio_set_cad_params. + * + * @param [in] context Chip implementation context + * @param [in] rx_period_in_rtc_step The length of Rx period + * @param [in] sleep_period_in_rtc_step The length of sleep period + * @param [in] mode The operation mode during Rx phase + * + * @returns Operation status + * + * @see lr11xx_radio_set_cad_params + */ +lr11xx_status_t lr11xx_radio_set_rx_duty_cycle_with_timings_in_rtc_step( const void* context, + const uint32_t rx_period_in_rtc_step, + const uint32_t sleep_period_in_rtc_step, + const lr11xx_radio_rx_duty_cycle_mode_t mode ); + +/*! + * @brief Set the Power Amplifier configuration + * + * It must be called prior using @ref lr11xx_radio_set_tx_params. + * + * @param [in] context Chip implementation context + * @param [in] pa_cfg The structure for PA configuration + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_pa_cfg( const void* context, const lr11xx_radio_pa_cfg_t* pa_cfg ); + +/*! + * @brief Define on which event the Rx timeout shall be stopped + * + * The two options are: + * - Syncword / Header detection + * - Preamble detection + * + * @param [in] context Chip implementation context + * @param [in] stop_timeout_on_preamble The choice of the event to be taken into account + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_stop_timeout_on_preamble( const void* context, const bool stop_timeout_on_preamble ); + +/*! + * @brief Start the CAD mode + * + * The LoRa packet type shall be selected before this function is called. The fallback mode is configured with + * lr11xx_radio_set_cad_params. + * + * @param [in] context Chip implementation context + * + * @returns Operation status + * + * @see lr11xx_radio_set_cad_params, lr11xx_radio_set_pkt_type + */ +lr11xx_status_t lr11xx_radio_set_cad( const void* context ); + +/*! + * @brief Set the device into Tx continuous wave (RF tone). + * + * A packet type shall be selected before this function is called. + * + * @param [in] context Chip implementation context + * + * @returns Operation status + * + * @see lr11xx_radio_set_pkt_type + */ +lr11xx_status_t lr11xx_radio_set_tx_cw( const void* context ); + +/*! + * @brief Set the device into Tx continuous preamble (modulated signal). + * + * @param [in] context Chip implementation context + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_tx_infinite_preamble( const void* context ); + +/*! + * @brief Configure the LoRa modem to issue a RX timeout after an exact number of symbols given in parameter if no LoRa + * modulation is detected + * + * @param [in] context Chip implementation context + * @param [in] nb_symbol number of symbols to compute the timeout + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_lora_sync_timeout( const void* context, const uint8_t nb_symbol ); + +/*! + * @brief Configure the seed and the polynomial used to compute CRC in GFSK packet + * + * @param [in] context Chip implementation context + * @param [in] seed Seed used to compute the CRC value + * @param [in] polynomial Polynomial used to compute the CRC value + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_gfsk_crc_params( const void* context, const uint32_t seed, const uint32_t polynomial ); + +/*! + * @brief Configure the whitening seed used in GFSK packet + * + * @param [in] context Chip implementation context + * @param [in] seed Whitening seed value + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_gfsk_whitening_seed( const void* context, const uint16_t seed ); + +/*! + * @brief Configure the boost mode in reception + * + * @param [in] context Chip implementation context + * @param [in] enable_boost_mode Boost mode activation + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_cfg_rx_boosted( const void* context, const bool enable_boost_mode ); + +/*! + * @brief Set RSSI calibration table + * + * @param [in] context Chip implementation context + * @param [in] rssi_cal_table RSSI calibration table + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_set_rssi_calibration( const void* context, + const lr11xx_radio_rssi_calibration_table_t* rssi_cal_table ); + +/*! + * @brief Gets the radio bw parameter for a given bandwidth in Hz + * + * @param [in] bw_in_hz Requested GFSK Rx bandwidth + * @param [out] bw_parameter Radio parameter immediately above requested bw_in_hz + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_get_gfsk_rx_bandwidth( uint32_t bw_in_hz, lr11xx_radio_gfsk_bw_t* bw_parameter ); + +/** + * @brief Compute the numerator for LoRa time-on-air computation. + * + * @remark To get the actual time-on-air in seconds, this value has to be divided by the LoRa bandwidth in Hertz. + * + * @param [in] pkt_p Pointer to the structure holding the LoRa packet parameters + * @param [in] mod_p Pointer to the structure holding the LoRa modulation parameters + * + * @returns LoRa time-on-air numerator + */ +uint32_t lr11xx_radio_get_lora_time_on_air_numerator( const lr11xx_radio_pkt_params_lora_t* pkt_p, + const lr11xx_radio_mod_params_lora_t* mod_p ); + +/** + * @brief Get the actual value in Hertz of a given LoRa bandwidth + * + * @param [in] bw LoRa bandwidth parameter + * + * @returns Actual LoRa bandwidth in Hertz + */ +uint32_t lr11xx_radio_get_lora_bw_in_hz( lr11xx_radio_lora_bw_t bw ); + +/*! + * @brief Get the time on air in ms for LoRa transmission + * + * @param [in] pkt_p Pointer to a structure holding the LoRa packet parameters + * @param [in] mod_p Pointer to a structure holding the LoRa modulation parameters + * + * @returns Time-on-air value in ms for LoRa transmission + */ +uint32_t lr11xx_radio_get_lora_time_on_air_in_ms( const lr11xx_radio_pkt_params_lora_t* pkt_p, + const lr11xx_radio_mod_params_lora_t* mod_p ); + +/** + * @brief Compute the numerator for GFSK time-on-air computation. + * + * @remark To get the actual time-on-air in seconds, this value has to be divided by the GFSK bitrate in bits per + * second. + * + * @param [in] pkt_p Pointer to the structure holding the GFSK packet parameters + * + * @returns GFSK time-on-air numerator + */ +uint32_t lr11xx_radio_get_gfsk_time_on_air_numerator( const lr11xx_radio_pkt_params_gfsk_t* pkt_p ); + +/** + * @brief Get the time on air in ms for GFSK transmission + * + * @param [in] pkt_p Pointer to a structure holding the GFSK packet parameters + * @param [in] mod_p Pointer to a structure holding the GFSK modulation parameters + * + * @returns Time-on-air value in ms for GFSK transmission + */ +uint32_t lr11xx_radio_get_gfsk_time_on_air_in_ms( const lr11xx_radio_pkt_params_gfsk_t* pkt_p, + const lr11xx_radio_mod_params_gfsk_t* mod_p ); + +/** + * @brief Get the number of RTC steps for a given time in millisecond + * + * @param [in] time_in_ms Timeout in millisecond + * + * @returns Number of RTC steps + */ +uint32_t lr11xx_radio_convert_time_in_ms_to_rtc_step( uint32_t time_in_ms ); + +/** + * @brief Get the information from the last received LoRa packet header (if @ref LR11XX_RADIO_LORA_PKT_EXPLICIT) or the + * locally configured settings (if @ref LR11XX_RADIO_LORA_PKT_IMPLICIT) + * + * @remark This function can be called only if @ref LR11XX_RADIO_PKT_TYPE_LORA is selected with @ref + * lr11xx_radio_set_pkt_type + * + * @param [in] context Chip implementation context + * @param [out] is_crc_present CRC configuration + * @param [out] cr LoRa coding rate + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_get_lora_rx_info( const void* context, bool* is_crc_present, lr11xx_radio_lora_cr_t* cr ); + +/*! + * @brief Apply the workaround for the high ACP limitation + * + * @param [in] context Chip implementation context + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_radio_apply_high_acp_workaround( const void* context ); + +#ifdef __cplusplus +} +#endif + +#endif // LR11XX_RADIO_H + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_radio_timings.c b/zephcore/adapters/radio/lr11xx/lr11xx_radio_timings.c new file mode 100644 index 0000000..a27eeeb --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_radio_timings.c @@ -0,0 +1,233 @@ +/** + * @file lr11xx_radio_timings.c + * + * @brief LR11XX timing helper functions implementation + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include "lr11xx_radio_timings.h" +#include "lr11xx_radio.h" + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE MACROS----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE CONSTANTS ------------------------------------------------------- + */ + +/** + * @brief Time in microsecond taken by the chip to process the Rx done interrupt + */ +#define RX_DONE_IRQ_PROCESSING_TIME_IN_US 74 + +/** + * @brief Time in microsecond taken by the chip to process the Tx done interrupt + */ +#define TX_DONE_IRQ_PROCESSING_TIME_IN_US 111 + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE TYPES ----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE VARIABLES ------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE FUNCTIONS DECLARATION ------------------------------------------- + */ + +/** + * @brief Get the power amplifier ramp time for a given power amplifier ramp time parameter + * + * @param [in] ramp_time Power amplifier ramp time parameter + * + * @returns Ramp time in microsecond + */ +static uint32_t lr11xx_radio_timings_get_pa_ramp_time_in_us( const lr11xx_radio_ramp_time_t ramp_time ); + +/** + * @brief Get the LoRa reception input delay + * + * @param [in] bw LoRa bandwidth + * + * @returns LoRa reception input delay in microsecond + */ +static uint32_t lr11xx_radio_timings_get_lora_rx_input_delay_in_us( lr11xx_radio_lora_bw_t bw ); + +/** + * @brief Get the LoRa symbol time + * + * @param [in] bw LoRa bandwidth + * @param [in] sf LoRa spreading factor + * + * @returns LoRa symbol time in microsecond + */ +static uint32_t lr11xx_radio_timings_get_lora_symb_time_in_us( const lr11xx_radio_lora_sf_t sf, + const lr11xx_radio_lora_bw_t bw ); + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS DEFINITION --------------------------------------------- + */ + +uint32_t lr11xx_radio_timings_get_delay_between_last_bit_sent_and_rx_done_in_us( + const lr11xx_radio_mod_params_lora_t* mod_params ) +{ + return lr11xx_radio_timings_get_lora_rx_input_delay_in_us( mod_params->bw ) + + 2 * lr11xx_radio_timings_get_lora_symb_time_in_us( mod_params->sf, mod_params->bw ) + + RX_DONE_IRQ_PROCESSING_TIME_IN_US; +} + +uint32_t lr11xx_radio_timings_get_delay_between_last_bit_sent_and_tx_done_in_us( + const lr11xx_radio_ramp_time_t ramp_time ) +{ + return lr11xx_radio_timings_get_pa_ramp_time_in_us( ramp_time ) + TX_DONE_IRQ_PROCESSING_TIME_IN_US; +} + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE FUNCTIONS DEFINITION -------------------------------------------- + */ + +static uint32_t lr11xx_radio_timings_get_pa_ramp_time_in_us( const lr11xx_radio_ramp_time_t ramp_time ) +{ + switch( ramp_time ) + { + case LR11XX_RADIO_RAMP_16_US: + { + return 16; + } + case LR11XX_RADIO_RAMP_32_US: + { + return 32; + } + case LR11XX_RADIO_RAMP_48_US: + { + return 48; + } + case LR11XX_RADIO_RAMP_64_US: + { + return 64; + } + case LR11XX_RADIO_RAMP_80_US: + { + return 80; + } + case LR11XX_RADIO_RAMP_96_US: + { + return 96; + } + case LR11XX_RADIO_RAMP_112_US: + { + return 112; + } + case LR11XX_RADIO_RAMP_128_US: + { + return 128; + } + case LR11XX_RADIO_RAMP_144_US: + { + return 144; + } + case LR11XX_RADIO_RAMP_160_US: + { + return 160; + } + case LR11XX_RADIO_RAMP_176_US: + { + return 176; + } + case LR11XX_RADIO_RAMP_192_US: + { + return 192; + } + case LR11XX_RADIO_RAMP_208_US: + { + return 208; + } + case LR11XX_RADIO_RAMP_240_US: + { + return 240; + } + case LR11XX_RADIO_RAMP_272_US: + { + return 272; + } + case LR11XX_RADIO_RAMP_304_US: + { + return 304; + } + default: + return 0; + } +} + +static uint32_t lr11xx_radio_timings_get_lora_rx_input_delay_in_us( lr11xx_radio_lora_bw_t bw ) +{ + switch( bw ) + { + case LR11XX_RADIO_LORA_BW_500: + { + return 16; + } + case LR11XX_RADIO_LORA_BW_250: + { + return 31; + } + case LR11XX_RADIO_LORA_BW_125: + { + return 57; + } + default: + { + return 0; + } + } +} + +static uint32_t lr11xx_radio_timings_get_lora_symb_time_in_us( const lr11xx_radio_lora_sf_t sf, + const lr11xx_radio_lora_bw_t bw ) +{ + return ( 1 << ( uint8_t ) sf ) * 1000000 / lr11xx_radio_get_lora_bw_in_hz( bw ); +} + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_radio_timings.h b/zephcore/adapters/radio/lr11xx/lr11xx_radio_timings.h new file mode 100644 index 0000000..6270d56 --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_radio_timings.h @@ -0,0 +1,95 @@ +/** + * @file lr11xx_radio_timings.h + * + * @brief LR11XX timing helper functions definition + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LR11XX_RADIO_TIMINGS_H +#define LR11XX_RADIO_TIMINGS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include "lr11xx_radio_types.h" + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC MACROS ----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC CONSTANTS -------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC TYPES ------------------------------------------------------------ + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS PROTOTYPES --------------------------------------------- + */ + +/** + * @brief Get the time between the last bit sent (on Tx side) and the Rx done event (on Rx side) + * + * @param [in] mod_params Pointer to a structure holding the LoRa modulation parameters used for the computation + * + * @returns Delay in microsecond + */ +uint32_t lr11xx_radio_timings_get_delay_between_last_bit_sent_and_rx_done_in_us( + const lr11xx_radio_mod_params_lora_t* mod_params ); + +/** + * @brief Get the time between the last bit sent and the Tx done event + * + * @param [in] ramp_time Power amplifier ramp time + * + * @returns Delay in microsecond + */ +uint32_t lr11xx_radio_timings_get_delay_between_last_bit_sent_and_tx_done_in_us( + const lr11xx_radio_ramp_time_t ramp_time ); + +#ifdef __cplusplus +} +#endif + +#endif // LR11XX_RADIO_TIMINGS_H + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_radio_types.h b/zephcore/adapters/radio/lr11xx/lr11xx_radio_types.h new file mode 100644 index 0000000..6f5c58d --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_radio_types.h @@ -0,0 +1,573 @@ +/*! + * @file lr11xx_radio_types.h + * + * @brief Radio driver types for LR11XX + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LR11XX_RADIO_TYPES_H +#define LR11XX_RADIO_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include +#include + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC MACROS ----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC CONSTANTS -------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC TYPES ------------------------------------------------------------ + */ + +/*! + * @brief Power Amplifier Selection values + * + * - Low-power Power Amplifier can reach up to 14dBm + * - High-power Power Amplifier can reach up to 22 dBm + */ +typedef enum +{ + LR11XX_RADIO_PA_SEL_LP = 0x00, //!< Low-power Power Amplifier + LR11XX_RADIO_PA_SEL_HP = 0x01, //!< High-power Power Amplifier + LR11XX_RADIO_PA_SEL_HF = 0x02, //!< High-frequency Power Amplifier +} lr11xx_radio_pa_selection_t; + +/*! + * @brief GFSK Address Filtering configurations + * + * If Address Filtering is enabled but a wrong address is received, therefore the reception is aborted and the address + * error flag of packet status is set. + */ +typedef enum +{ + LR11XX_RADIO_GFSK_ADDRESS_FILTERING_DISABLE = 0x00, //!< Filter deactivated + LR11XX_RADIO_GFSK_ADDRESS_FILTERING_NODE_ADDRESS = 0x01, //!< Filter on Node Address + LR11XX_RADIO_GFSK_ADDRESS_FILTERING_NODE_AND_BROADCAST_ADDRESSES = + 0x02, //!< Filtering on Node and Broadcast addresses +} lr11xx_radio_gfsk_address_filtering_t; + +/*! + * @brief Chip mode after successfull transmission or reception + * + * Unused for RX duty cycle and AutoTxRx operations + */ +typedef enum +{ + LR11XX_RADIO_FALLBACK_STDBY_RC = 0x01, //!< Standby RC (Default) + LR11XX_RADIO_FALLBACK_STDBY_XOSC = 0x02, //!< Standby XOSC + LR11XX_RADIO_FALLBACK_FS = 0x03 //!< FS +} lr11xx_radio_fallback_modes_t; + +/*! + * @brief Ramping time for PA + * + * This parameter is the ramping time of the PA. A high value improves spectral quality. + */ +typedef enum +{ + LR11XX_RADIO_RAMP_16_US = 0x00, //!< 16 us Ramp Time + LR11XX_RADIO_RAMP_32_US = 0x01, //!< 32 us Ramp Time + LR11XX_RADIO_RAMP_48_US = 0x02, //!< 48 us Ramp Time (Default) + LR11XX_RADIO_RAMP_64_US = 0x03, //!< 64 us Ramp Time + LR11XX_RADIO_RAMP_80_US = 0x04, //!< 80 us Ramp Time + LR11XX_RADIO_RAMP_96_US = 0x05, //!< 96 us Ramp Time + LR11XX_RADIO_RAMP_112_US = 0x06, //!< 112 us Ramp Time + LR11XX_RADIO_RAMP_128_US = 0x07, //!< 128 us Ramp Time + LR11XX_RADIO_RAMP_144_US = 0x08, //!< 144 us Ramp Time + LR11XX_RADIO_RAMP_160_US = 0x09, //!< 160 us Ramp Time + LR11XX_RADIO_RAMP_176_US = 0x0A, //!< 176 us Ramp Time + LR11XX_RADIO_RAMP_192_US = 0x0B, //!< 192 us Ramp Time + LR11XX_RADIO_RAMP_208_US = 0x0C, //!< 208 us Ramp Time + LR11XX_RADIO_RAMP_240_US = 0x0D, //!< 240 us Ramp Time + LR11XX_RADIO_RAMP_272_US = 0x0E, //!< 272 us Ramp Time + LR11XX_RADIO_RAMP_304_US = 0x0F, //!< 304 us Ramp Time +} lr11xx_radio_ramp_time_t; + +/*! + * @brief LoRa network type configuration + */ +typedef enum +{ + LR11XX_RADIO_LORA_NETWORK_PRIVATE = 0x00, //!< LoRa private network + LR11XX_RADIO_LORA_NETWORK_PUBLIC = 0x01, //!< LoRa public network +} lr11xx_radio_lora_network_type_t; + +/*! + * @brief LoRa Spreading Factor configurations + */ +typedef enum +{ + LR11XX_RADIO_LORA_SF5 = 0x05, //!< Spreading Factor 5 + LR11XX_RADIO_LORA_SF6 = 0x06, //!< Spreading Factor 6 + LR11XX_RADIO_LORA_SF7 = 0x07, //!< Spreading Factor 7 + LR11XX_RADIO_LORA_SF8 = 0x08, //!< Spreading Factor 8 + LR11XX_RADIO_LORA_SF9 = 0x09, //!< Spreading Factor 9 + LR11XX_RADIO_LORA_SF10 = 0x0A, //!< Spreading Factor 10 + LR11XX_RADIO_LORA_SF11 = 0x0B, //!< Spreading Factor 11 + LR11XX_RADIO_LORA_SF12 = 0x0C, //!< Spreading Factor 12 +} lr11xx_radio_lora_sf_t; + +/*! + * @brief LoRa Bandwidth configurations + */ +typedef enum +{ + LR11XX_RADIO_LORA_BW_10 = 0x08, //!< Bandwidth 10.42 kHz + LR11XX_RADIO_LORA_BW_15 = 0x01, //!< Bandwidth 15.63 kHz + LR11XX_RADIO_LORA_BW_20 = 0x09, //!< Bandwidth 20.83 kHz + LR11XX_RADIO_LORA_BW_31 = 0x02, //!< Bandwidth 31.25 kHz + LR11XX_RADIO_LORA_BW_41 = 0x0A, //!< Bandwidth 41.67 kHz + LR11XX_RADIO_LORA_BW_62 = 0x03, //!< Bandwidth 62.50 kHz + LR11XX_RADIO_LORA_BW_125 = 0x04, //!< Bandwidth 125.00 kHz + LR11XX_RADIO_LORA_BW_250 = 0x05, //!< Bandwidth 250.00 kHz + LR11XX_RADIO_LORA_BW_500 = 0x06, //!< Bandwidth 500.00 kHz + LR11XX_RADIO_LORA_BW_200 = 0x0D, //!< Bandwidth 203.00 kHz, 2G4 and compatible with LR112x chips only + LR11XX_RADIO_LORA_BW_400 = 0x0E, //!< Bandwidth 406.00 kHz, 2G4 and compatible with LR112x chips only + LR11XX_RADIO_LORA_BW_800 = 0x0F, //!< Bandwidth 812.00 kHz, 2G4 and compatible with LR112x chips only +} lr11xx_radio_lora_bw_t; + +/*! + * @brief LoRa Coding Rate configurations + */ +typedef enum +{ + LR11XX_RADIO_LORA_NO_CR = 0x00, //!< No Coding Rate + LR11XX_RADIO_LORA_CR_4_5 = 0x01, //!< Coding Rate 4/5 Short Interleaver + LR11XX_RADIO_LORA_CR_4_6 = 0x02, //!< Coding Rate 4/6 Short Interleaver + LR11XX_RADIO_LORA_CR_4_7 = 0x03, //!< Coding Rate 4/7 Short Interleaver + LR11XX_RADIO_LORA_CR_4_8 = 0x04, //!< Coding Rate 4/8 Short Interleaver + LR11XX_RADIO_LORA_CR_LI_4_5 = 0x05, //!< Coding Rate 4/5 Long Interleaver + LR11XX_RADIO_LORA_CR_LI_4_6 = 0x06, //!< Coding Rate 4/6 Long Interleaver + LR11XX_RADIO_LORA_CR_LI_4_8 = 0x07, //!< Coding Rate 4/8 Long Interleaver +} lr11xx_radio_lora_cr_t; + +/*! + * @brief Values for intermediary mode + */ +typedef enum +{ + LR11XX_RADIO_MODE_SLEEP = 0x00, //!< Sleep / Not recommended with LR1110 FW from 0x0303 to 0x0307 and LR1120 FW + //!< 0x0101 in case of transition from Rx to Tx in LoRa + LR11XX_RADIO_MODE_STANDBY_RC = 0x01, //!< Standby RC + LR11XX_RADIO_MODE_STANDBY_XOSC = 0x02, //!< Standby XOSC + LR11XX_RADIO_MODE_FS = 0x03 //!< Frequency Synthesis +} lr11xx_radio_intermediary_mode_t; + +/*! + * @brief GFSK Cyclic Redundancy Check configurations + * + * If this value is set to something other than CRC_OFF, a CRC is automatically computed and added after the end of the + * payload on transmitter side. On receiver side, the CRC check is automatically processed. + */ +typedef enum +{ + LR11XX_RADIO_GFSK_CRC_OFF = 0x01, //!< CRC check deactivated + LR11XX_RADIO_GFSK_CRC_1_BYTE = 0x00, + LR11XX_RADIO_GFSK_CRC_2_BYTES = 0x02, + LR11XX_RADIO_GFSK_CRC_1_BYTE_INV = 0x04, + LR11XX_RADIO_GFSK_CRC_2_BYTES_INV = 0x06, +} lr11xx_radio_gfsk_crc_type_t; + +/*! + * @brief GFSK data whitening configurations + */ +typedef enum +{ + LR11XX_RADIO_GFSK_DC_FREE_OFF = 0x00, //!< Whitening deactivated + LR11XX_RADIO_GFSK_DC_FREE_WHITENING = 0x01, //!< Whitening enabled + LR11XX_RADIO_GFSK_DC_FREE_WHITENING_SX128X_COMP = 0x03, //!< Whitening enabled - SX128x compatibility +} lr11xx_radio_gfsk_dc_free_t; + +/*! + * @brief GFSK Header Type configurations + * + * This parameter indicates whether or not the payload length is sent and read over the air. + * + * If the payload length is known beforehand by both transmitter and receiver, therefore there is no need to send it + * over the air. Otherwise, setting this parameter to LR11XX_RADIO_GFSK_PKT_VAR_LEN will make the modem to automatically + * prepand a byte containing the payload length to the the payload on transmitter side. On receiver side, this first + * byte is read to set the payload length to read. + * + * This configuration is only available for GFSK packet types. + */ +typedef enum +{ + LR11XX_RADIO_GFSK_PKT_FIX_LEN = 0x00, //!< Payload length is not sent/read over the air + LR11XX_RADIO_GFSK_PKT_VAR_LEN = 0x01, //!< Payload length is sent/read over the air + LR11XX_RADIO_GFSK_PKT_VAR_LEN_SX128X_COMP = + 0x02, //!< Payload length is sent/read over the air - SX128x compatibility +} lr11xx_radio_gfsk_pkt_len_modes_t; + +/*! + * @brief GFSK Preamble Detector Length configurations + * + * This parameter sets the minimum length of preamble bits to be received to continue reception of incoming packet. If a + * packet with preamble length lower than this value is being received, the reception stops without generating IRQ. + * + * This parameter has no impact on TX operations. + */ +typedef enum +{ + LR11XX_RADIO_GFSK_PREAMBLE_DETECTOR_OFF = 0x00, + LR11XX_RADIO_GFSK_PREAMBLE_DETECTOR_MIN_8BITS = 0x04, + LR11XX_RADIO_GFSK_PREAMBLE_DETECTOR_MIN_16BITS = 0x05, + LR11XX_RADIO_GFSK_PREAMBLE_DETECTOR_MIN_24BITS = 0x06, + LR11XX_RADIO_GFSK_PREAMBLE_DETECTOR_MIN_32BITS = 0x07 +} lr11xx_radio_gfsk_preamble_detector_t; + +/*! + * @brief LoRa Cyclic Redundancy Check configurations + */ +typedef enum +{ + LR11XX_RADIO_LORA_CRC_OFF = 0x00, //!< CRC deactivated + LR11XX_RADIO_LORA_CRC_ON = 0x01, //!< CRC activated +} lr11xx_radio_lora_crc_t; + +/*! + * @brief LoRa Header type configurations + */ +typedef enum +{ + LR11XX_RADIO_LORA_PKT_EXPLICIT = 0x00, //!< Explicit header: transmitted over the air + LR11XX_RADIO_LORA_PKT_IMPLICIT = 0x01, //!< Implicit header: not transmitted over the air +} lr11xx_radio_lora_pkt_len_modes_t; + +/*! + * @brief LoRa IQ mode configurations + * + * LoRa IQ modes are mutually exclusives: a physical packet sent with standard IQ will not be received by a receiver + * configured with inverted IQ. + */ +typedef enum +{ + LR11XX_RADIO_LORA_IQ_STANDARD = 0x00, //!< IQ standard + LR11XX_RADIO_LORA_IQ_INVERTED = 0x01, //!< IQ inverted +} lr11xx_radio_lora_iq_t; + +/*! + * @brief Packet type values + */ +typedef enum +{ + LR11XX_RADIO_PKT_NONE = 0x00, //!< State after cold start, Wi-Fi or GNSS capture + LR11XX_RADIO_PKT_TYPE_GFSK = 0x01, //!< GFSK modulation + LR11XX_RADIO_PKT_TYPE_LORA = 0x02, //!< LoRa modulation +} lr11xx_radio_pkt_type_t; + +/*! + * @brief Select power amplifier supply source + */ +typedef enum +{ + LR11XX_RADIO_PA_REG_SUPPLY_VREG = 0x00, //!< Power amplifier supplied by the main regulator + LR11XX_RADIO_PA_REG_SUPPLY_VBAT = 0x01 //!< Power amplifier supplied by the battery +} lr11xx_radio_pa_reg_supply_t; + +/*! + * @brief RX Duty Cycle Modes + */ +typedef enum +{ + LR11XX_RADIO_RX_DUTY_CYCLE_MODE_RX = 0x00, //!< LoRa/GFSK: Uses Rx for listening to packets + LR11XX_RADIO_RX_DUTY_CYCLE_MODE_CAD = 0x01, //!< Only in LoRa: Uses CAD to listen for over-the-air activity +} lr11xx_radio_rx_duty_cycle_mode_t; + +/*! + * @brief GFSK Bandwidth configurations + */ +typedef enum +{ + LR11XX_RADIO_GFSK_BW_4800 = 0x1F, //!< Bandwidth 4.8 kHz DSB + LR11XX_RADIO_GFSK_BW_5800 = 0x17, //!< Bandwidth 5.8 kHz DSB + LR11XX_RADIO_GFSK_BW_7300 = 0x0F, //!< Bandwidth 7.3 kHz DSB + LR11XX_RADIO_GFSK_BW_9700 = 0x1E, //!< Bandwidth 9.7 kHz DSB + LR11XX_RADIO_GFSK_BW_11700 = 0x16, //!< Bandwidth 11.7 kHz DSB + LR11XX_RADIO_GFSK_BW_14600 = 0x0E, //!< Bandwidth 14.6 kHz DSB + LR11XX_RADIO_GFSK_BW_19500 = 0x1D, //!< Bandwidth 19.5 kHz DSB + LR11XX_RADIO_GFSK_BW_23400 = 0x15, //!< Bandwidth 23.4 kHz DSB + LR11XX_RADIO_GFSK_BW_29300 = 0x0D, //!< Bandwidth 29.3 kHz DSB + LR11XX_RADIO_GFSK_BW_39000 = 0x1C, //!< Bandwidth 39.0 kHz DSB + LR11XX_RADIO_GFSK_BW_46900 = 0x14, //!< Bandwidth 46.9 kHz DSB + LR11XX_RADIO_GFSK_BW_58600 = 0x0C, //!< Bandwidth 58.6 kHz DSB + LR11XX_RADIO_GFSK_BW_78200 = 0x1B, //!< Bandwidth 78.2 kHz DSB + LR11XX_RADIO_GFSK_BW_93800 = 0x13, //!< Bandwidth 93.8 kHz DSB + LR11XX_RADIO_GFSK_BW_117300 = 0x0B, //!< Bandwidth 117.3 kHz DSB + LR11XX_RADIO_GFSK_BW_156200 = 0x1A, //!< Bandwidth 156.2 kHz DSB + LR11XX_RADIO_GFSK_BW_187200 = 0x12, //!< Bandwidth 187.2 kHz DSB + LR11XX_RADIO_GFSK_BW_234300 = 0x0A, //!< Bandwidth 232.3 kHz DSB + LR11XX_RADIO_GFSK_BW_312000 = 0x19, //!< Bandwidth 312.0 kHz DSB + LR11XX_RADIO_GFSK_BW_373600 = 0x11, //!< Bandwidth 373.6 kHz DSB + LR11XX_RADIO_GFSK_BW_467000 = 0x09 //!< Bandwidth 467.0 kHz DSB +} lr11xx_radio_gfsk_bw_t; + +/*! + * @brief Possible automatic actions when Channel Activity Detection operations terminate + * + * For RADIO_EXIT_MODE_CAD_RX, LR11XX enters RX mode on activity detected. The timeout value for this RX operation is + * defined as: + * + * \f$ 31.25us \times timeout \f$ + * + * With \f$ timeout \f$ defined in RadioCadParams_t::timeout + * + * If the CAD operation is negative with RADIO_CAD_EXIT_MODE_RX or if CAD operation is positive with + * RADIO_CAD_EXIT_MODE_TX, therefore the LR11XX enters Standby RC mode. + */ +typedef enum +{ + LR11XX_RADIO_CAD_EXIT_MODE_STANDBYRC = 0x00, //!< Enter standby RC mode after CAD operation + LR11XX_RADIO_CAD_EXIT_MODE_RX = 0x01, //!< Enter in RX mode if an activity is detected + LR11XX_RADIO_CAD_EXIT_MODE_TX = 0x10, //!< Enter in TX mode if no activity is detected +} lr11xx_radio_cad_exit_mode_t; + +/*! + * @brief Pulse shape configurations + */ +typedef enum +{ + LR11XX_RADIO_GFSK_PULSE_SHAPE_OFF = 0x00, //!< No filter applied + LR11XX_RADIO_GFSK_PULSE_SHAPE_BT_03 = 0x08, //!< Gaussian BT 0.3 + LR11XX_RADIO_GFSK_PULSE_SHAPE_BT_05 = 0x09, //!< Gaussian BT 0.5 + LR11XX_RADIO_GFSK_PULSE_SHAPE_BT_07 = 0x0A, //!< Gaussian BT 0.7 + LR11XX_RADIO_GFSK_PULSE_SHAPE_BT_1 = 0x0B //!< Gaussian BT 1.0 +} lr11xx_radio_gfsk_pulse_shape_t; + +/*! + * @brief Channel Activity Detection parameters + * + * Parameters detPeak and detMin are to be used for tuning the sensitivity of Channel Activity Detection. It depends on + * Spreading Factor, Bandwidth and symbolNum. + * + * For detPeak, the 5 MSBits are encoding the integer part, the 3 LSBits are encoding 1/8 of the decimal part. For + * instance, \f$detPeak = 50\f$ (= 0x32) leads to a ratio being \f$6 + 2 * 1/8 = 6.25\f$. + * + * detMin is unit free and represents the ratio between the minimal power of a correlation peak and measurement gain + * that can be considered as a peak detection. It helps to avoid detection on noise. Authorized values a from 0 to 181. + */ +typedef struct lr11xx_radio_cad_params_s +{ + uint8_t cad_symb_nb; //!< Number of symbols used for CAD detection + uint8_t cad_detect_peak; //!< Ratio for CAD between correlator peak and average + //!< (Default 0x32) + uint8_t cad_detect_min; //!< Minimum power of the correlation peak to be + //!< considered as a positive CAD (Default 0x0A) + lr11xx_radio_cad_exit_mode_t cad_exit_mode; //!< Automated action on CAD completion + uint32_t cad_timeout; //!< Value used to compute timeout +} lr11xx_radio_cad_params_t; + +/*! + * @brief Status of GFSK received packet + */ +typedef struct lr11xx_radio_pkt_status_gfsk_s +{ + int8_t rssi_sync_in_dbm; //!< RSSI value latched on detection of the last received packet Sync Address + int8_t rssi_avg_in_dbm; //!< RSSI averaged over the payload of the last received packet + uint8_t rx_len_in_bytes; //!< Length of the last received packet [Bytes] + bool is_addr_err; //!< Address filtering status. Asserted if received packet address does not match node address + //!< nor broadcast address + bool is_crc_err; //!< CRC status of the current packet (applicable only in RX, with CRC enabled) + bool is_len_err; //!< Asserted when the length of last received packet is greater than the maximal length + //!< (applicable only in RX with variable length packet) + bool is_abort_err; //!< Asserted when the current packet has been aborted (applicable in RX and TX) + bool is_received; //!< Asserted when packet reception is done (applicable in RX) + bool is_sent; //!< Asserted when packet transmission is done (applicable in TX) +} lr11xx_radio_pkt_status_gfsk_t; + +/*! + * @brief Status of received packet + */ +typedef struct lr11xx_radio_pkt_status_lora_s +{ + int8_t rssi_pkt_in_dbm; //!< Average RSSI over last received packet. + int8_t snr_pkt_in_db; //!< SNR estimated on last received packet. + int8_t signal_rssi_pkt_in_dbm; //!< RSSI of last packet latched after +} lr11xx_radio_pkt_status_lora_t; + +/*! + * @brief Length and offset of received packet + */ +typedef struct lr11xx_radio_rx_buffer_status_s +{ + uint8_t pld_len_in_bytes; //!< Length of received packet [Bytes] + uint8_t buffer_start_pointer; //!< Offset in the reception buffer of + //!< first byte received [Bytes] +} lr11xx_radio_rx_buffer_status_t; + +/*! + * @brief GFSK packet statistic structure + */ +typedef struct lr11xx_radio_stats_gfsk_s +{ + uint16_t nb_pkt_received; //!< Total number of received packets + uint16_t nb_pkt_crc_error; //!< Total number of received packets with CRC error + uint16_t nb_pkt_len_error; //!< Total number of received packets with a length error +} lr11xx_radio_stats_gfsk_t; + +/*! + * @brief LoRa packet statistic structure + */ +typedef struct lr11xx_radio_stats_lora_s +{ + uint16_t nb_pkt_received; //!< Total number of received packets + uint16_t nb_pkt_crc_error; //!< Total number of received packets with CRC error + uint16_t nb_pkt_header_error; //!< Total number of packets with header error + uint16_t nb_pkt_falsesync; //!< Total number of false sync +} lr11xx_radio_stats_lora_t; + +/*! + * @brief Modulation configuration for GFSK packet + */ +typedef struct lr11xx_radio_mod_params_gfsk_s +{ + uint32_t br_in_bps; //!< GFSK bitrate [bit/s] + lr11xx_radio_gfsk_pulse_shape_t pulse_shape; //!< GFSK pulse shape + lr11xx_radio_gfsk_bw_t bw_dsb_param; //!< GFSK bandwidth + uint32_t fdev_in_hz; //!< GFSK frequency deviation [Hz] +} lr11xx_radio_mod_params_gfsk_t; + +/*! + * @brief Modulation configuration for LoRa packet + */ +typedef struct lr11xx_radio_mod_params_lora_s +{ + lr11xx_radio_lora_sf_t sf; //!< LoRa spreading factor + lr11xx_radio_lora_bw_t bw; //!< LoRa bandwidth + lr11xx_radio_lora_cr_t cr; //!< LoRa coding rate + uint8_t ldro; //!< LoRa LDRO +} lr11xx_radio_mod_params_lora_t; + +/*! + * @brief Packet parameter configuration for GFSK packets + */ +typedef struct lr11xx_radio_pkt_params_gfsk_s +{ + uint16_t preamble_len_in_bits; //!< GFSK Preamble length [bits] + lr11xx_radio_gfsk_preamble_detector_t preamble_detector; //!< GFSK Preamble detection configuration + uint8_t sync_word_len_in_bits; //!< GFSK Syncword length [bits] + lr11xx_radio_gfsk_address_filtering_t address_filtering; //!< GFSK Address filtering/comparison configuration + lr11xx_radio_gfsk_pkt_len_modes_t header_type; //!< GFSK Header type configuration + uint8_t pld_len_in_bytes; //!< GFSK Payload length [bytes] + lr11xx_radio_gfsk_crc_type_t crc_type; //!< GFSK CRC configuration + lr11xx_radio_gfsk_dc_free_t dc_free; //!< GFSK Whitening configuration +} lr11xx_radio_pkt_params_gfsk_t; + +/*! + * @brief Packet parameter configuration for LoRa packets + */ +typedef struct lr11xx_radio_pkt_params_lora_s +{ + uint16_t preamble_len_in_symb; //!< LoRa Preamble length [symbols] + lr11xx_radio_lora_pkt_len_modes_t header_type; //!< LoRa Header type configuration + uint8_t pld_len_in_bytes; //!< LoRa Payload length [bytes] + lr11xx_radio_lora_crc_t crc; //!< LoRa CRC configuration + lr11xx_radio_lora_iq_t iq; //!< LoRa IQ configuration +} lr11xx_radio_pkt_params_lora_t; + +/*! + * @brief Configuration of Power Amplifier + * + * @ref pa_duty_cycle controls the duty cycle of Power Amplifier according to: + * \f$ dutycycle = 0.2 + 0.04 \times pa_duty_cycle \f$ + * It can be used to adapt the TX multi-band operation using a single-matching network. + * + * The allowed duty cycle values for LPA are from 0.2 to 0.48 (by step of 0.04). Therefore possible values for + * pa_duty_cycle go from 0 to 7. + * + * The allowed duty cycle values for HPA go from 0.2 to 0.36 (by step of 0.04). Therefore in this case, the possible + * values for pa_duty_cycle go from 0 to 4. + * + * @ref pa_hp_sel controls the number of slices for HPA according to: \f$ \#slices = pa_hp_sel + 1 \f$ + */ +typedef struct lr11xx_radio_pa_cfg_s +{ + lr11xx_radio_pa_selection_t pa_sel; //!< Power Amplifier selection + lr11xx_radio_pa_reg_supply_t pa_reg_supply; //!< Power Amplifier regulator supply source + uint8_t pa_duty_cycle; //!< Power Amplifier duty cycle (Default 0x04) + uint8_t pa_hp_sel; //!< Number of slices for HPA (Default 0x07) +} lr11xx_radio_pa_cfg_t; + +/*! + * @brief RSSI calibration table + */ +typedef struct lr11xx_radio_rssi_calibration_table_s +{ + struct + { + uint8_t g4; + uint8_t g5; + uint8_t g6; + uint8_t g7; + uint8_t g8; + uint8_t g9; + uint8_t g10; + uint8_t g11; + uint8_t g12; + uint8_t g13; + uint8_t g13hp1; + uint8_t g13hp2; + uint8_t g13hp3; + uint8_t g13hp4; + uint8_t g13hp5; + uint8_t g13hp6; + uint8_t g13hp7; + } gain_tune; //!< Used to set gain tune value for RSSI calibration + + int16_t gain_offset; //!< Used to set gain offset value for RSSI calibration +} lr11xx_radio_rssi_calibration_table_t; + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS PROTOTYPES --------------------------------------------- + */ + +#ifdef __cplusplus +} +#endif + +#endif // LR11XX_RADIO_TYPES_H + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_regmem.c b/zephcore/adapters/radio/lr11xx/lr11xx_regmem.c new file mode 100644 index 0000000..3c5312a --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_regmem.c @@ -0,0 +1,314 @@ +/*! + * @file lr11xx_regmem.c + * + * @brief Register/memory driver implementation for LR11XX + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include "lr11xx_regmem.h" +#include "lr11xx_hal.h" + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE MACROS----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE CONSTANTS ------------------------------------------------------- + */ + +#define LR11XX_REGMEM_CLEAR_RXBUFFER_CMD_LENGTH 2 +#define LR11XX_REGMEM_WRITE_REGMEM32_CMD_LENGTH ( 2 + 4 ) +#define LR11XX_REGMEM_READ_REGMEM32_CMD_LENGTH ( 2 + 4 + 1 ) +#define LR11XX_REGMEM_WRITE_MEM8_CMD_LENGTH ( 2 + 4 ) +#define LR11XX_REGMEM_READ_MEM8_CMD_LENGTH ( 2 + 4 + 1 ) +#define LR11XX_REGMEM_WRITE_BUFFER8_CMD_LENGTH ( 2 ) +#define LR11XX_REGMEM_READ_BUFFER8_CMD_LENGTH ( 2 + 2 ) +#define LR11XX_REGMEM_WRITE_REGMEM32_MASK_CMD_LENGTH ( 2 + 4 + 4 + 4 ) + +#define LR11XX_REGMEM_BUFFER_SIZE_MAX ( 256 ) + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE TYPES ----------------------------------------------------------- + */ + +/*! + * @brief Operating codes for register and memory related operations + */ +enum +{ + LR11XX_REGMEM_WRITE_REGMEM32_OC = 0x0105, + LR11XX_REGMEM_READ_REGMEM32_OC = 0x0106, + LR11XX_REGMEM_WRITE_MEM8_OC = 0x0107, + LR11XX_REGMEM_READ_MEM8_OC = 0x0108, + LR11XX_REGMEM_WRITE_BUFFER8_OC = 0x0109, + LR11XX_REGMEM_READ_BUFFER8_OC = 0x010A, + LR11XX_REGMEM_CLEAR_RXBUFFER_OC = 0x010B, + LR11XX_REGMEM_WRITE_REGMEM32_MASK_OC = 0x010C, +}; + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE VARIABLES ------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE FUNCTIONS DECLARATION ------------------------------------------- + */ + +/*! + * @brief Helper function that fill both cbuffer with opcode and memory address + * + * It is typically used in read/write regmem32 functions. + * + * @warning It is up to the caller to ensure cbuffer is big enough to contain opcode and address! + */ +static void lr11xx_regmem_fill_cbuffer_opcode_address( uint8_t* cbuffer, uint16_t opcode, uint32_t address ); + +/*! + * @brief Helper function that fill both cbuffer with opcode memory address, and data length to read + * + * It is typically used in read functions. + * + * @warning It is up to the caller to ensure cbuffer is big enough to contain opcode and address! + */ +static void lr11xx_regmem_fill_cbuffer_opcode_address_length( uint8_t* cbuffer, uint16_t opcode, uint32_t address, + uint8_t length ); + +/*! + * @brief Helper function that fill both cbuffer with data + * + * It is typically used in write write regmem32 functions. + * + * @warning It is up to the caller to ensure cdata is big enough to contain all data! + */ +static void lr11xx_regmem_fill_cdata( uint8_t* cdata, const uint32_t* data, uint8_t data_length ); + +/*! + * @brief Helper function that fill both cbuffer and cdata buffers with opcode, memory address and data + * + * It is typically used to factorize and write regmem32 operations. Behind the scene it calls the other helpers + * lr11xx_regmem_fill_cbuffer_opcode_address and lr11xx_regmem_fill_cdata. + * + * @warning It is up to the caller to ensure cbuffer and cdata are big enough to contain their respective information! + */ +static void lr11xx_regmem_fill_cbuffer_cdata_opcode_address_data( uint8_t* cbuffer, uint8_t* cdata, uint16_t opcode, + uint32_t address, const uint32_t* data, + uint8_t data_length ); + +/*! + * @brief Helper function that convert an array of uint8_t into an array of uint32_t + * + * Typically used in the read function returning uint32_t array. + * + * @warning It is up to the caller to ensure the raw_buffer is of length at least "out_buffer_length * + * sizeof(uint32_t)"! + */ +static void lr11xx_regmem_fill_out_buffer_from_raw_buffer( uint32_t* out_buffer, const uint8_t* raw_buffer, + uint8_t out_buffer_length ); + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS DEFINITION --------------------------------------------- + */ + +lr11xx_status_t lr11xx_regmem_write_regmem32( const void* context, const uint32_t address, const uint32_t* buffer, + const uint8_t length ) +{ + uint8_t cbuffer[LR11XX_REGMEM_WRITE_REGMEM32_CMD_LENGTH]; + uint8_t cdata[LR11XX_REGMEM_BUFFER_SIZE_MAX]; + + lr11xx_regmem_fill_cbuffer_cdata_opcode_address_data( cbuffer, cdata, LR11XX_REGMEM_WRITE_REGMEM32_OC, address, + buffer, length ); + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_REGMEM_WRITE_REGMEM32_CMD_LENGTH, cdata, + length * sizeof( uint32_t ) ); +} + +lr11xx_status_t lr11xx_regmem_read_regmem32( const void* context, const uint32_t address, uint32_t* buffer, + const uint8_t length ) +{ + uint8_t cbuffer[LR11XX_REGMEM_READ_REGMEM32_CMD_LENGTH]; + lr11xx_status_t status = LR11XX_STATUS_ERROR; + + lr11xx_regmem_fill_cbuffer_opcode_address_length( cbuffer, LR11XX_REGMEM_READ_REGMEM32_OC, address, length ); + + status = ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_REGMEM_READ_REGMEM32_CMD_LENGTH, + ( uint8_t* ) buffer, length * sizeof( uint32_t ) ); + + if( status == LR11XX_STATUS_OK ) + { + lr11xx_regmem_fill_out_buffer_from_raw_buffer( buffer, ( const uint8_t* ) buffer, length ); + } + + return status; +} + +lr11xx_status_t lr11xx_regmem_write_mem8( const void* context, const uint32_t address, const uint8_t* buffer, + const uint8_t length ) +{ + uint8_t cbuffer[LR11XX_REGMEM_WRITE_MEM8_CMD_LENGTH]; + + lr11xx_regmem_fill_cbuffer_opcode_address( cbuffer, LR11XX_REGMEM_WRITE_MEM8_OC, address ); + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_REGMEM_WRITE_MEM8_CMD_LENGTH, buffer, + length ); +} + +lr11xx_status_t lr11xx_regmem_read_mem8( const void* context, const uint32_t address, uint8_t* buffer, + const uint8_t length ) +{ + uint8_t cbuffer[LR11XX_REGMEM_READ_MEM8_CMD_LENGTH]; + + lr11xx_regmem_fill_cbuffer_opcode_address_length( cbuffer, LR11XX_REGMEM_READ_MEM8_OC, address, length ); + + return ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_REGMEM_READ_MEM8_CMD_LENGTH, buffer, length ); +} + +lr11xx_status_t lr11xx_regmem_write_buffer8( const void* context, const uint8_t* buffer, const uint8_t length ) +{ + const uint8_t cbuffer[LR11XX_REGMEM_WRITE_BUFFER8_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_REGMEM_WRITE_BUFFER8_OC >> 8 ), + ( uint8_t ) ( LR11XX_REGMEM_WRITE_BUFFER8_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_REGMEM_WRITE_BUFFER8_CMD_LENGTH, buffer, + length ); +} + +lr11xx_status_t lr11xx_regmem_read_buffer8( const void* context, uint8_t* buffer, const uint8_t offset, + const uint8_t length ) +{ + const uint8_t cbuffer[LR11XX_REGMEM_READ_BUFFER8_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_REGMEM_READ_BUFFER8_OC >> 8 ), + ( uint8_t ) ( LR11XX_REGMEM_READ_BUFFER8_OC >> 0 ), + offset, + length, + }; + + return ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_REGMEM_READ_BUFFER8_CMD_LENGTH, buffer, + length ); +} + +lr11xx_status_t lr11xx_regmem_clear_rxbuffer( const void* context ) +{ + const uint8_t cbuffer[LR11XX_REGMEM_CLEAR_RXBUFFER_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_REGMEM_CLEAR_RXBUFFER_OC >> 8 ), + ( uint8_t ) ( LR11XX_REGMEM_CLEAR_RXBUFFER_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_REGMEM_CLEAR_RXBUFFER_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_regmem_write_regmem32_mask( const void* context, const uint32_t address, const uint32_t mask, + const uint32_t data ) +{ + uint8_t cbuffer[LR11XX_REGMEM_WRITE_REGMEM32_MASK_CMD_LENGTH]; + + lr11xx_regmem_fill_cbuffer_opcode_address( cbuffer, LR11XX_REGMEM_WRITE_REGMEM32_MASK_OC, address ); + + cbuffer[6] = ( uint8_t ) ( mask >> 24 ); + cbuffer[7] = ( uint8_t ) ( mask >> 16 ); + cbuffer[8] = ( uint8_t ) ( mask >> 8 ); + cbuffer[9] = ( uint8_t ) ( mask >> 0 ); + + cbuffer[10] = ( uint8_t ) ( data >> 24 ); + cbuffer[11] = ( uint8_t ) ( data >> 16 ); + cbuffer[12] = ( uint8_t ) ( data >> 8 ); + cbuffer[13] = ( uint8_t ) ( data >> 0 ); + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_REGMEM_WRITE_REGMEM32_MASK_CMD_LENGTH, 0, 0 ); +} + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE FUNCTIONS DEFINITION -------------------------------------------- + */ + +void lr11xx_regmem_fill_cbuffer_opcode_address( uint8_t* cbuffer, uint16_t opcode, uint32_t address ) +{ + cbuffer[0] = ( uint8_t ) ( opcode >> 8 ); + cbuffer[1] = ( uint8_t ) ( opcode >> 0 ); + + cbuffer[2] = ( uint8_t ) ( address >> 24 ); + cbuffer[3] = ( uint8_t ) ( address >> 16 ); + cbuffer[4] = ( uint8_t ) ( address >> 8 ); + cbuffer[5] = ( uint8_t ) ( address >> 0 ); +} + +void lr11xx_regmem_fill_cbuffer_opcode_address_length( uint8_t* cbuffer, uint16_t opcode, uint32_t address, + uint8_t length ) +{ + lr11xx_regmem_fill_cbuffer_opcode_address( cbuffer, opcode, address ); + cbuffer[6] = length; +} + +void lr11xx_regmem_fill_cdata( uint8_t* cdata, const uint32_t* data, uint8_t data_length ) +{ + for( uint16_t index = 0; index < data_length; index++ ) + { + uint8_t* cdata_local = &cdata[index * sizeof( uint32_t )]; + + cdata_local[0] = ( uint8_t ) ( data[index] >> 24 ); + cdata_local[1] = ( uint8_t ) ( data[index] >> 16 ); + cdata_local[2] = ( uint8_t ) ( data[index] >> 8 ); + cdata_local[3] = ( uint8_t ) ( data[index] >> 0 ); + } +} + +void lr11xx_regmem_fill_cbuffer_cdata_opcode_address_data( uint8_t* cbuffer, uint8_t* cdata, uint16_t opcode, + uint32_t address, const uint32_t* data, uint8_t data_length ) +{ + lr11xx_regmem_fill_cbuffer_opcode_address( cbuffer, opcode, address ); + lr11xx_regmem_fill_cdata( cdata, data, data_length ); +} + +void lr11xx_regmem_fill_out_buffer_from_raw_buffer( uint32_t* out_buffer, const uint8_t* raw_buffer, + uint8_t out_buffer_length ) +{ + for( uint8_t out_index = 0; out_index < out_buffer_length; out_index++ ) + { + const uint8_t* raw_buffer_local = &raw_buffer[out_index * 4]; + + out_buffer[out_index] = ( ( uint32_t ) raw_buffer_local[0] << 24 ) + + ( ( uint32_t ) raw_buffer_local[1] << 16 ) + ( ( uint32_t ) raw_buffer_local[2] << 8 ) + + ( ( uint32_t ) raw_buffer_local[3] << 0 ); + } +} + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_regmem.h b/zephcore/adapters/radio/lr11xx/lr11xx_regmem.h new file mode 100644 index 0000000..9a89eb9 --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_regmem.h @@ -0,0 +1,202 @@ +/*! + * @file lr11xx_regmem.h + * + * @brief Register/memory driver definition for LR11XX + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LR11XX_REGMEM_H +#define LR11XX_REGMEM_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include +#include "lr11xx_types.h" + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC MACROS ----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC CONSTANTS -------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC TYPES ------------------------------------------------------------ + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS PROTOTYPES --------------------------------------------- + */ + +/*! + * @brief Write words into register memory space of LR11XX. + * + * A word is 32-bit long. The writing operations write contiguously in register memory, starting at the address + * provided. + * + * @param [in] context Chip implementation context + * @param [in] address The register memory address to start writing operation + * @param [in] data The buffer of words to write into memory. Its size must be enough to contain length words. + * @param [in] length Number of words to write into memory + * + * @returns Operation status + * + * @see lr11xx_regmem_read_regmem32 + */ +lr11xx_status_t lr11xx_regmem_write_regmem32( const void* context, const uint32_t address, const uint32_t* buffer, + const uint8_t length ); + +/*! + * @brief Read words into register memory space of LR11XX. + * + * A word is 32-bit long. The reading operations read contiguously from register memory, starting at the address + * provided. + * + * @param [in] context Chip implementation context + * @param [in] address The register memory address to start reading operation + * @param [in] length Number of words to read from memory + * @param [out] buffer Pointer to a words array to be filled with content from memory. Its size must be enough to + * contain at least length words. + * + * @returns Operation status + * + * @see lr11xx_regmem_write_regmem32 + */ +lr11xx_status_t lr11xx_regmem_read_regmem32( const void* context, const uint32_t address, uint32_t* buffer, + const uint8_t length ); + +/*! + * @brief Write bytes into register memory space of LR11XX. + * + * A byte is 8-bit long. The writing operations write contiguously in register memory, starting at the address provided. + * + * @param [in] context Chip implementation context + * @param [in] address The register memory address to start writing operation + * @param [in] data The buffer of bytes to write into memory. Its size must be enough to contain length bytes + * @param [in] length Number of bytes to write into memory + * + * @returns Operation status + * + * @see lr11xx_regmem_read_mem8 + */ +lr11xx_status_t lr11xx_regmem_write_mem8( const void* context, const uint32_t address, const uint8_t* buffer, + const uint8_t length ); + +/*! + * @brief Read bytes into register memory space of LR11XX. + * + * A byte is 8-bit long. The reading operations read contiguously from register memory, starting at the address + * provided. + * + * @param [in] context Chip implementation context + * @param [in] address The register memory address to start reading operation + * @param [in] length Number of bytes to read from memory + * @param [in] buffer Pointer to a byte array to be filled with content from memory. Its size must be enough to contain + * at least length bytes + * + * @returns Operation status + * + * @see lr11xx_regmem_write_mem8 + */ +lr11xx_status_t lr11xx_regmem_read_mem8( const void* context, const uint32_t address, uint8_t* buffer, + const uint8_t length ); + +/*! + * @brief Write bytes into radio TX buffer memory space of LR11XX. + * + * @param [in] context Chip implementation context + * @param [in] data The buffer of bytes to write into radio buffer. Its size must be enough to contain length bytes + * @param [in] length Number of bytes to write into radio buffer + * + * @returns Operation status + * + * @see lr11xx_regmem_read_buffer8 + */ +lr11xx_status_t lr11xx_regmem_write_buffer8( const void* context, const uint8_t* buffer, const uint8_t length ); + +/*! + * @brief Read bytes from radio RX buffer memory space of LR11XX. + * + * @param [in] context Chip implementation context + * @param [in] offset Memory offset to start reading + * @param [in] length Number of bytes to read from radio buffer + * @param [in] data Pointer to a byte array to be filled with content from radio buffer. Its size must be enough to + * contain at least length bytes + * + * @returns Operation status + * + * @see lr11xx_regmem_write_buffer8 + */ +lr11xx_status_t lr11xx_regmem_read_buffer8( const void* context, uint8_t* buffer, const uint8_t offset, + const uint8_t length ); + +/*! + * @brief Clear radio RX buffer + * + * Set to 0x00 all content of the radio RX buffer + * + * @param [in] context Chip implementation context + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_regmem_clear_rxbuffer( const void* context ); + +/*! + * @brief Read-modify-write data at given register/memory address + * + * @param [in] context Chip implementation context + * @param [in] address The register memory address to be modified + * @param [in] mask The mask to be applied on read data + * @param [in] data The data to be written + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_regmem_write_regmem32_mask( const void* context, const uint32_t address, const uint32_t mask, + const uint32_t data ); + +#ifdef __cplusplus +} +#endif + +#endif // LR11XX_REGMEM_H + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_system.c b/zephcore/adapters/radio/lr11xx/lr11xx_system.c new file mode 100644 index 0000000..cc7f372 --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_system.c @@ -0,0 +1,659 @@ +/*! + * @file lr11xx_system.c + * + * @brief System driver implementation for LR11XX + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include + +#include "lr11xx_system.h" +#include "lr11xx_hal.h" + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE MACROS----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE CONSTANTS ------------------------------------------------------- + */ + +#define LR11XX_SYSTEM_GET_VERSION_CMD_LENGTH ( 2 ) +#define LR11XX_SYSTEM_GET_ERRORS_CMD_LENGTH ( 2 ) +#define LR11XX_SYSTEM_CLEAR_ERRORS_CMD_LENGTH ( 2 ) +#define LR11XX_SYSTEM_CALIBRATE_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_SYSTEM_SET_REGMODE_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_SYSTEM_CALIBRATE_IMAGE_CMD_LENGTH ( 2 + 2 ) +#define LR11XX_SYSTEM_SET_DIO_AS_RF_SWITCH_CMD_LENGTH ( 2 + 8 ) +#define LR11XX_SYSTEM_SET_DIO_IRQ_PARAMS_CMD_LENGTH ( 2 + 8 ) +#define LR11XX_SYSTEM_CLEAR_IRQ_CMD_LENGTH ( 2 + 4 ) +#define LR11XX_SYSTEM_CFG_LFCLK_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_SYSTEM_SET_TCXO_MODE_CMD_LENGTH ( 2 + 4 ) +#define LR11XX_SYSTEM_REBOOT_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_SYSTEM_GET_VBAT_CMD_LENGTH ( 2 ) +#define LR11XX_SYSTEM_GET_TEMP_CMD_LENGTH ( 2 ) +#define LR11XX_SYSTEM_SET_SLEEP_CMD_LENGTH ( 2 + 5 ) +#define LR11XX_SYSTEM_SET_STANDBY_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_SYSTEM_SET_FS_CMD_LENGTH ( 2 ) +#define LR11XX_SYSTEM_ERASE_INFOPAGE_CMD_LENGTH ( 2 + 1 ) +#define LR11XX_SYSTEM_WRITE_INFOPAGE_CMD_LENGTH ( 2 + 3 ) +#define LR11XX_SYSTEM_READ_INFOPAGE_CMD_LENGTH ( 2 + 4 ) +#define LR11XX_SYSTEM_READ_UID_CMD_LENGTH ( 2 ) +#define LR11XX_SYSTEM_READ_JOIN_EUI_CMD_LENGTH ( 2 ) +#define LR11XX_SYSTEM_READ_PIN_CMD_LENGTH ( 2 ) +#define LR11XX_SYSTEM_READ_PIN_CUSTOM_EUI_CMD_LENGTH ( LR11XX_SYSTEM_READ_PIN_CMD_LENGTH + 17 ) +#define LR11XX_SYSTEM_GET_RANDOM_CMD_LENGTH ( 2 ) +#define LR11XX_SYSTEM_ENABLE_SPI_CRC_CMD_LENGTH ( 3 ) +#define LR11XX_SYSTEM_DRIVE_DIO_IN_SLEEP_MODE_CMD_LENGTH ( 3 ) + +#define LR11XX_SYSTEM_GET_STATUS_DIRECT_READ_LENGTH ( 6 ) + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE TYPES ----------------------------------------------------------- + */ + +/*! + * @brief Operating codes for system-related operations + */ +enum +{ + LR11XX_SYSTEM_GET_STATUS_OC = 0x0100, + LR11XX_SYSTEM_GET_VERSION_OC = 0x0101, + LR11XX_SYSTEM_GET_ERRORS_OC = 0x010D, + LR11XX_SYSTEM_CLEAR_ERRORS_OC = 0x010E, + LR11XX_SYSTEM_CALIBRATE_OC = 0x010F, + LR11XX_SYSTEM_SET_REGMODE_OC = 0x0110, + LR11XX_SYSTEM_CALIBRATE_IMAGE_OC = 0x0111, + LR11XX_SYSTEM_SET_DIO_AS_RF_SWITCH_OC = 0x0112, + LR11XX_SYSTEM_SET_DIOIRQPARAMS_OC = 0x0113, + LR11XX_SYSTEM_CLEAR_IRQ_OC = 0x0114, + LR11XX_SYSTEM_CFG_LFCLK_OC = 0x0116, + LR11XX_SYSTEM_SET_TCXO_MODE_OC = 0x0117, + LR11XX_SYSTEM_REBOOT_OC = 0x0118, + LR11XX_SYSTEM_GET_VBAT_OC = 0x0119, + LR11XX_SYSTEM_GET_TEMP_OC = 0x011A, + LR11XX_SYSTEM_SET_SLEEP_OC = 0x011B, + LR11XX_SYSTEM_SET_STANDBY_OC = 0x011C, + LR11XX_SYSTEM_SET_FS_OC = 0x011D, + LR11XX_SYSTEM_GET_RANDOM_OC = 0x0120, + LR11XX_SYSTEM_ERASE_INFOPAGE_OC = 0x0121, + LR11XX_SYSTEM_WRITE_INFOPAGE_OC = 0x0122, + LR11XX_SYSTEM_READ_INFOPAGE_OC = 0x0123, + LR11XX_SYSTEM_READ_UID_OC = 0x0125, + LR11XX_SYSTEM_READ_JOIN_EUI_OC = 0x0126, + LR11XX_SYSTEM_READ_PIN_OC = 0x0127, + LR11XX_SYSTEM_ENABLE_SPI_CRC_OC = 0x0128, + LR11XX_SYSTEM_DRIVE_DIO_IN_SLEEP_MODE_OC = 0x012A, +}; + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE VARIABLES ------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE FUNCTIONS DECLARATION ------------------------------------------- + */ + +/*! + * @brief Fill stat1 structure with data from stat1_byte + * + * @param [in] stat1_byte stat1 byte + * @param [out] stat1 stat1 structure + */ +static void lr11xx_system_convert_stat1_byte_to_enum( uint8_t stat1_byte, lr11xx_system_stat1_t* stat1 ); + +/*! + * @brief Fill stat2 structure with data from stat2_byte + * + * @param [in] stat2_byte stat2 byte + * @param [out] stat2 stat2 structure + */ +static void lr11xx_system_convert_stat2_byte_to_enum( uint8_t stat2_byte, lr11xx_system_stat2_t* stat2 ); + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS DEFINITION --------------------------------------------- + */ + +lr11xx_status_t lr11xx_system_reset( const void* context ) +{ + return ( lr11xx_status_t ) lr11xx_hal_reset( context ); +} + +lr11xx_status_t lr11xx_system_get_status( const void* context, lr11xx_system_stat1_t* stat1, + lr11xx_system_stat2_t* stat2, lr11xx_system_irq_mask_t* irq_status ) +{ + uint8_t data[LR11XX_SYSTEM_GET_STATUS_DIRECT_READ_LENGTH]; + lr11xx_status_t status; + + status = ( lr11xx_status_t ) lr11xx_hal_direct_read( context, data, LR11XX_SYSTEM_GET_STATUS_DIRECT_READ_LENGTH ); + + if( status == LR11XX_STATUS_OK ) + { + lr11xx_system_convert_stat1_byte_to_enum( data[0], stat1 ); + lr11xx_system_convert_stat2_byte_to_enum( data[1], stat2 ); + if( irq_status != NULL ) + { + *irq_status = ( ( lr11xx_system_irq_mask_t ) data[2] << 24 ) + + ( ( lr11xx_system_irq_mask_t ) data[3] << 16 ) + + ( ( lr11xx_system_irq_mask_t ) data[4] << 8 ) + ( ( lr11xx_system_irq_mask_t ) data[5] << 0 ); + } + } + + return status; +} + +lr11xx_status_t lr11xx_system_clear_reset_status_info( const void* context ) +{ + uint8_t cbuffer[2] = { + ( uint8_t ) ( LR11XX_SYSTEM_GET_STATUS_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_GET_STATUS_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, sizeof( cbuffer ), 0, 0 ); +} + +lr11xx_status_t lr11xx_system_get_version( const void* context, lr11xx_system_version_t* version ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_GET_VERSION_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_GET_VERSION_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_GET_VERSION_OC >> 0 ), + }; + uint8_t rbuffer[LR11XX_SYSTEM_VERSION_LENGTH] = { 0x00 }; + + const lr11xx_status_t status = ( lr11xx_status_t ) lr11xx_hal_read( + context, cbuffer, LR11XX_SYSTEM_GET_VERSION_CMD_LENGTH, rbuffer, LR11XX_SYSTEM_VERSION_LENGTH ); + + if( status == LR11XX_STATUS_OK ) + { + version->hw = rbuffer[0]; + version->type = rbuffer[1]; + version->fw = ( ( uint16_t ) rbuffer[2] << 8 ) + ( uint16_t ) rbuffer[3]; + } + + return status; +} + +lr11xx_status_t lr11xx_system_get_errors( const void* context, lr11xx_system_errors_t* errors ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_GET_ERRORS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_GET_ERRORS_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_GET_ERRORS_OC >> 0 ), + }; + uint8_t rbuffer[sizeof( errors )] = { 0x00 }; + + const lr11xx_status_t status = ( lr11xx_status_t ) lr11xx_hal_read( + context, cbuffer, LR11XX_SYSTEM_GET_ERRORS_CMD_LENGTH, rbuffer, sizeof( *errors ) ); + + if( status == LR11XX_STATUS_OK ) + { + *errors = ( ( uint16_t ) rbuffer[0] << 8 ) + ( uint16_t ) rbuffer[1]; + } + + return status; +} + +lr11xx_status_t lr11xx_system_clear_errors( const void* context ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_CLEAR_ERRORS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_CLEAR_ERRORS_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_CLEAR_ERRORS_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_CLEAR_ERRORS_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_calibrate( const void* context, const uint8_t calib_param ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_CALIBRATE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_CALIBRATE_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_CALIBRATE_OC >> 0 ), + calib_param, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_CALIBRATE_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_set_reg_mode( const void* context, const lr11xx_system_reg_mode_t reg_mode ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_SET_REGMODE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_SET_REGMODE_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_SET_REGMODE_OC >> 0 ), + ( uint8_t ) reg_mode, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_SET_REGMODE_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_calibrate_image( const void* context, const uint8_t freq1, const uint8_t freq2 ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_CALIBRATE_IMAGE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_CALIBRATE_IMAGE_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_CALIBRATE_IMAGE_OC >> 0 ), + freq1, + freq2, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_CALIBRATE_IMAGE_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_calibrate_image_in_mhz( const void* context, const uint16_t freq1_in_mhz, + const uint16_t freq2_in_mhz ) +{ + // Perform a floor() to get a value for freq1 corresponding to a frequency lower than or equal to freq1_in_mhz + const uint8_t freq1 = freq1_in_mhz / LR11XX_SYSTEM_IMAGE_CALIBRATION_STEP_IN_MHZ; + + // Perform a ceil() to get a value for freq2 corresponding to a frequency higher than or equal to freq2_in_mhz + const uint8_t freq2 = ( freq2_in_mhz + LR11XX_SYSTEM_IMAGE_CALIBRATION_STEP_IN_MHZ - 1 ) / + LR11XX_SYSTEM_IMAGE_CALIBRATION_STEP_IN_MHZ; + + return lr11xx_system_calibrate_image( context, freq1, freq2 ); +} + +lr11xx_status_t lr11xx_system_set_dio_as_rf_switch( const void* context, + const lr11xx_system_rfswitch_cfg_t* rf_switch_cfg ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_SET_DIO_AS_RF_SWITCH_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_SET_DIO_AS_RF_SWITCH_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_SET_DIO_AS_RF_SWITCH_OC >> 0 ), + rf_switch_cfg->enable, + rf_switch_cfg->standby, + rf_switch_cfg->rx, + rf_switch_cfg->tx, + rf_switch_cfg->tx_hp, + rf_switch_cfg->tx_hf, + rf_switch_cfg->gnss, + rf_switch_cfg->wifi, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_SET_DIO_AS_RF_SWITCH_CMD_LENGTH, 0, + 0 ); +} + +lr11xx_status_t lr11xx_system_set_dio_irq_params( const void* context, + const lr11xx_system_irq_mask_t irqs_to_enable_dio1, + const lr11xx_system_irq_mask_t irqs_to_enable_dio2 ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_SET_DIO_IRQ_PARAMS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_SET_DIOIRQPARAMS_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_SET_DIOIRQPARAMS_OC >> 0 ), + ( uint8_t ) ( irqs_to_enable_dio1 >> 24 ), + ( uint8_t ) ( irqs_to_enable_dio1 >> 16 ), + ( uint8_t ) ( irqs_to_enable_dio1 >> 8 ), + ( uint8_t ) ( irqs_to_enable_dio1 >> 0 ), + ( uint8_t ) ( irqs_to_enable_dio2 >> 24 ), + ( uint8_t ) ( irqs_to_enable_dio2 >> 16 ), + ( uint8_t ) ( irqs_to_enable_dio2 >> 8 ), + ( uint8_t ) ( irqs_to_enable_dio2 >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_SET_DIO_IRQ_PARAMS_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_clear_irq_status( const void* context, const lr11xx_system_irq_mask_t irqs_to_clear ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_CLEAR_IRQ_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_CLEAR_IRQ_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_CLEAR_IRQ_OC >> 0 ), + ( uint8_t ) ( irqs_to_clear >> 24 ), + ( uint8_t ) ( irqs_to_clear >> 16 ), + ( uint8_t ) ( irqs_to_clear >> 8 ), + ( uint8_t ) ( irqs_to_clear >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_CLEAR_IRQ_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_get_and_clear_irq_status( const void* context, lr11xx_system_irq_mask_t* irq ) +{ + lr11xx_system_irq_mask_t lr11xx_irq_mask = LR11XX_SYSTEM_IRQ_NONE; + + lr11xx_status_t status = lr11xx_system_get_irq_status( context, &lr11xx_irq_mask ); + + if( ( status == LR11XX_STATUS_OK ) && ( lr11xx_irq_mask != 0 ) ) + { + status = lr11xx_system_clear_irq_status( context, lr11xx_irq_mask ); + } + if( ( status == LR11XX_STATUS_OK ) && ( irq != NULL ) ) + { + *irq = lr11xx_irq_mask; + } + + return status; +} + +lr11xx_status_t lr11xx_system_cfg_lfclk( const void* context, const lr11xx_system_lfclk_cfg_t lfclock_cfg, + const bool wait_for_32k_ready ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_CFG_LFCLK_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_CFG_LFCLK_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_CFG_LFCLK_OC >> 0 ), + ( uint8_t ) ( lfclock_cfg | ( wait_for_32k_ready << 2 ) ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_CFG_LFCLK_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_set_tcxo_mode( const void* context, const lr11xx_system_tcxo_supply_voltage_t tune, + const uint32_t timeout ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_SET_TCXO_MODE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_SET_TCXO_MODE_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_SET_TCXO_MODE_OC >> 0 ), + ( uint8_t ) tune, + ( uint8_t ) ( timeout >> 16 ), + ( uint8_t ) ( timeout >> 8 ), + ( uint8_t ) ( timeout >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_SET_TCXO_MODE_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_reboot( const void* context, const bool stay_in_bootloader ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_REBOOT_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_REBOOT_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_REBOOT_OC >> 0 ), + ( stay_in_bootloader == true ) ? 0x03 : 0x00, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_REBOOT_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_get_vbat( const void* context, uint8_t* vbat ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_GET_VBAT_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_GET_VBAT_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_GET_VBAT_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_SYSTEM_GET_VBAT_CMD_LENGTH, vbat, + sizeof( *vbat ) ); +} + +lr11xx_status_t lr11xx_system_get_temp( const void* context, uint16_t* temp ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_GET_TEMP_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_GET_TEMP_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_GET_TEMP_OC >> 0 ), + }; + uint8_t rbuffer[sizeof( uint16_t )] = { 0x00 }; + + const lr11xx_status_t status = ( lr11xx_status_t ) lr11xx_hal_read( + context, cbuffer, LR11XX_SYSTEM_GET_TEMP_CMD_LENGTH, rbuffer, sizeof( uint16_t ) ); + + if( status == LR11XX_STATUS_OK ) + { + *temp = ( ( uint16_t ) rbuffer[0] << 8 ) + ( uint16_t ) rbuffer[1]; + } + + return status; +} + +lr11xx_status_t lr11xx_system_set_sleep( const void* context, const lr11xx_system_sleep_cfg_t sleep_cfg, + const uint32_t sleep_time ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_SET_SLEEP_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_SET_SLEEP_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_SET_SLEEP_OC >> 0 ), + ( sleep_cfg.is_rtc_timeout << 1 ) + sleep_cfg.is_warm_start, + ( uint8_t ) ( sleep_time >> 24 ), + ( uint8_t ) ( sleep_time >> 16 ), + ( uint8_t ) ( sleep_time >> 8 ), + ( uint8_t ) ( sleep_time >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_SET_SLEEP_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_set_standby( const void* context, const lr11xx_system_standby_cfg_t standby_cfg ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_SET_STANDBY_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_SET_STANDBY_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_SET_STANDBY_OC >> 0 ), + ( uint8_t ) standby_cfg, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_SET_STANDBY_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_wakeup( const void* context ) +{ + return ( lr11xx_status_t ) lr11xx_hal_wakeup( context ); +} + +lr11xx_status_t lr11xx_system_set_fs( const void* context ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_SET_FS_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_SET_FS_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_SET_FS_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_SET_FS_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_erase_infopage( const void* context, const lr11xx_system_infopage_id_t infopage_id ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_ERASE_INFOPAGE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_ERASE_INFOPAGE_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_ERASE_INFOPAGE_OC >> 0 ), + ( uint8_t ) infopage_id, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_ERASE_INFOPAGE_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_write_infopage( const void* context, const lr11xx_system_infopage_id_t infopage_id, + const uint16_t address, const uint32_t* data, const uint8_t length ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_WRITE_INFOPAGE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_WRITE_INFOPAGE_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_WRITE_INFOPAGE_OC >> 0 ), + ( uint8_t ) infopage_id, + ( uint8_t ) ( address >> 8 ), + ( uint8_t ) ( address >> 0 ), + }; + uint8_t cdata[256]; + + for( uint16_t index = 0; index < length; index++ ) + { + uint8_t* cdata_local = &cdata[index * sizeof( uint32_t )]; + + cdata_local[0] = ( uint8_t ) ( data[index] >> 24 ); + cdata_local[1] = ( uint8_t ) ( data[index] >> 16 ); + cdata_local[2] = ( uint8_t ) ( data[index] >> 8 ); + cdata_local[3] = ( uint8_t ) ( data[index] >> 0 ); + } + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_WRITE_INFOPAGE_CMD_LENGTH, cdata, + length * sizeof( uint32_t ) ); +} + +lr11xx_status_t lr11xx_system_read_infopage( const void* context, const lr11xx_system_infopage_id_t infopage_id, + const uint16_t address, uint32_t* data, const uint8_t length ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_READ_INFOPAGE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_READ_INFOPAGE_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_READ_INFOPAGE_OC >> 0 ), + ( uint8_t ) infopage_id, + ( uint8_t ) ( address >> 8 ), + ( uint8_t ) ( address >> 0 ), + length, + }; + + const lr11xx_status_t status = ( lr11xx_status_t ) lr11xx_hal_read( + context, cbuffer, LR11XX_SYSTEM_READ_INFOPAGE_CMD_LENGTH, ( uint8_t* ) data, length * sizeof( *data ) ); + + if( status == LR11XX_STATUS_OK ) + { + for( uint8_t index = 0; index < length; index++ ) + { + uint8_t* buffer_local = ( uint8_t* ) &data[index]; + + data[index] = ( ( uint32_t ) buffer_local[0] << 24 ) + ( ( uint32_t ) buffer_local[1] << 16 ) + + ( ( uint32_t ) buffer_local[2] << 8 ) + ( ( uint32_t ) buffer_local[3] << 0 ); + } + } + + return status; +} + +lr11xx_status_t lr11xx_system_read_uid( const void* context, lr11xx_system_uid_t unique_identifier ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_READ_UID_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_READ_UID_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_READ_UID_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_SYSTEM_READ_UID_CMD_LENGTH, unique_identifier, + LR11XX_SYSTEM_UID_LENGTH ); +} + +lr11xx_status_t lr11xx_system_read_join_eui( const void* context, lr11xx_system_join_eui_t join_eui ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_READ_JOIN_EUI_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_READ_JOIN_EUI_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_READ_JOIN_EUI_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_SYSTEM_READ_JOIN_EUI_CMD_LENGTH, join_eui, + LR11XX_SYSTEM_JOIN_EUI_LENGTH ); +} + +lr11xx_status_t lr11xx_system_read_pin( const void* context, lr11xx_system_pin_t pin ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_READ_PIN_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_READ_PIN_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_READ_PIN_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_SYSTEM_READ_PIN_CMD_LENGTH, pin, + LR11XX_SYSTEM_PIN_LENGTH ); +} + +lr11xx_status_t lr11xx_system_read_pin_custom_eui( const void* context, lr11xx_system_uid_t device_eui, + lr11xx_system_join_eui_t join_eui, uint8_t rfu, + lr11xx_system_pin_t pin ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_READ_PIN_CUSTOM_EUI_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_READ_PIN_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_READ_PIN_OC >> 0 ), + device_eui[0], + device_eui[1], + device_eui[2], + device_eui[3], + device_eui[4], + device_eui[5], + device_eui[6], + device_eui[7], + join_eui[0], + join_eui[1], + join_eui[2], + join_eui[3], + join_eui[4], + join_eui[5], + join_eui[6], + join_eui[7], + rfu, + }; + + return ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_SYSTEM_READ_PIN_CUSTOM_EUI_CMD_LENGTH, pin, + LR11XX_SYSTEM_PIN_LENGTH ); +} + +lr11xx_status_t lr11xx_system_get_random_number( const void* context, uint32_t* random_number ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_GET_RANDOM_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_GET_RANDOM_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_GET_RANDOM_OC >> 0 ), + }; + + return ( lr11xx_status_t ) lr11xx_hal_read( context, cbuffer, LR11XX_SYSTEM_GET_RANDOM_CMD_LENGTH, + ( uint8_t* ) random_number, sizeof( uint32_t ) ); +} + +lr11xx_status_t lr11xx_system_enable_spi_crc( const void* context, bool enable_crc ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_ENABLE_SPI_CRC_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_ENABLE_SPI_CRC_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_ENABLE_SPI_CRC_OC >> 0 ), + ( enable_crc == true ) ? 0x01 : 0x00, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_ENABLE_SPI_CRC_CMD_LENGTH, 0, 0 ); +} + +lr11xx_status_t lr11xx_system_drive_dio_in_sleep_mode( const void* context, bool enable_drive ) +{ + const uint8_t cbuffer[LR11XX_SYSTEM_DRIVE_DIO_IN_SLEEP_MODE_CMD_LENGTH] = { + ( uint8_t ) ( LR11XX_SYSTEM_DRIVE_DIO_IN_SLEEP_MODE_OC >> 8 ), + ( uint8_t ) ( LR11XX_SYSTEM_DRIVE_DIO_IN_SLEEP_MODE_OC >> 0 ), + ( enable_drive == true ) ? 0x01 : 0x00, + }; + + return ( lr11xx_status_t ) lr11xx_hal_write( context, cbuffer, LR11XX_SYSTEM_DRIVE_DIO_IN_SLEEP_MODE_CMD_LENGTH, 0, + 0 ); +} + +/* + * ----------------------------------------------------------------------------- + * --- PRIVATE FUNCTIONS DEFINITION -------------------------------------------- + */ + +static void lr11xx_system_convert_stat1_byte_to_enum( uint8_t stat1_byte, lr11xx_system_stat1_t* stat1 ) +{ + if( stat1 != NULL ) + { + stat1->is_interrupt_active = ( ( stat1_byte & 0x01 ) != 0 ) ? true : false; + stat1->command_status = ( lr11xx_system_command_status_t ) ( stat1_byte >> 1 ); + } +} + +static void lr11xx_system_convert_stat2_byte_to_enum( uint8_t stat2_byte, lr11xx_system_stat2_t* stat2 ) +{ + if( stat2 != NULL ) + { + stat2->is_running_from_flash = ( ( stat2_byte & 0x01 ) != 0 ) ? true : false; + stat2->chip_mode = ( lr11xx_system_chip_modes_t ) ( ( stat2_byte & 0x0F ) >> 1 ); + stat2->reset_status = ( lr11xx_system_reset_status_t ) ( ( stat2_byte & 0xF0 ) >> 4 ); + } +} + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_system.h b/zephcore/adapters/radio/lr11xx/lr11xx_system.h new file mode 100644 index 0000000..0142689 --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_system.h @@ -0,0 +1,585 @@ +/*! + * @file lr11xx_system.h + * + * @brief System driver definition for LR11XX + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LR11XX_SYSTEM_H +#define LR11XX_SYSTEM_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include "lr11xx_system_types.h" +#include "lr11xx_types.h" + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC MACROS ----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC CONSTANTS -------------------------------------------------------- + */ + +/*! + * @brief Frequency step in MHz used to compute the image calibration parameter + * + * @see lr11xx_system_calibrate_image_in_mhz + */ +#define LR11XX_SYSTEM_IMAGE_CALIBRATION_STEP_IN_MHZ 4 + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC TYPES ------------------------------------------------------------ + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS PROTOTYPES --------------------------------------------- + */ + +/*! + * @brief Reset the radio + * + * @param [in] context Chip implementation context + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_reset( const void* context ); + +/** + * @brief Wake the radio up from sleep mode. + * + * @param [in] context Chip implementation context. + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_wakeup( const void* context ); + +/*! + * @brief Return stat1, stat2, and irq_status + * + * @param [in] context Chip implementation context + * @param [out] stat1 Pointer to a variable for holding stat1. Can be NULL. + * @param [out] stat2 Pointer to a variable for holding stat2. Can be NULL. + * @param [out] irq_status Pointer to a variable for holding irq_status. Can be NULL. + * + * @returns Operation status + * + * @remark To simplify system integration, this function does not actually execute the GetStatus command, which would + * require bidirectional SPI communication. It obtains the stat1, stat2, and irq_status values by performing an ordinary + * SPI read (which is required to send null/NOP bytes on the MOSI line). This is possible since the LR11XX returns these + * values automatically whenever a read that does not directly follow a response-carrying command is performed. + * Unlike with the GetStatus command, however, the reset status information is NOT cleared by this command. The function + * @ref lr11xx_system_clear_reset_status_info may be used for this purpose when necessary. + */ +lr11xx_status_t lr11xx_system_get_status( const void* context, lr11xx_system_stat1_t* stat1, + lr11xx_system_stat2_t* stat2, lr11xx_system_irq_mask_t* irq_status ); + +/*! + * @brief Clear the reset status information stored in stat2 + * + * @param [in] context Chip implementation context + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_clear_reset_status_info( const void* context ); + +/*! + * @brief Return irq_status + * + * @param [in] context Chip implementation context + * @param [out] irq_status irq_status status variable + * + * @returns Operation status + */ +static inline lr11xx_status_t lr11xx_system_get_irq_status( const void* context, lr11xx_system_irq_mask_t* irq_status ) +{ + return lr11xx_system_get_status( context, 0, 0, irq_status ); +} + +/*! + * @brief Return the version of the system (hardware and software) + * + * @param [in] context Chip implementation context + * @param [out] version Pointer to the structure holding the system version + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_get_version( const void* context, lr11xx_system_version_t* version ); + +/*! + * @brief Return the system errors + * + * Errors may be fixed following: + * - calibration error can be fixed by attempting another RC calibration; + * - XOsc related errors may be due to hardware problems, can be fixed by reset; + * - PLL lock related errors can be due to not-locked PLL, or by attempting to use an out-of-band frequency, can be + * fixed by executing a PLL calibration, or by using other frequencies. + * + * @param [in] context Chip implementation context + * @param [out] errors Pointer to a value holding error flags + * + * @returns Operation status + * + * @see lr11xx_system_calibrate, lr11xx_system_calibrate_image, lr11xx_system_clear_errors + */ +lr11xx_status_t lr11xx_system_get_errors( const void* context, uint16_t* errors ); + +/*! + * @brief Clear all error flags pending. + * + * This function cannot be used to clear flags individually. + * + * @param [in] context Chip implementation context + * + * @returns Operation status + * + * @see lr11xx_system_get_errors + */ +lr11xx_status_t lr11xx_system_clear_errors( const void* context ); + +/*! + * @brief lr11xx_system_calibrate the requested blocks + * + * This function can be called in any mode of the chip. + * + * The chip will return to standby RC mode on exit. Potential calibration issues can be read out with + * lr11xx_system_get_errors command. + * + * @param [in] context Chip implementation context + * @param [in] calib_param Structure holding the reference to blocks to be calibrated + * + * @returns Operation status + * + * @see lr11xx_system_get_errors + */ +lr11xx_status_t lr11xx_system_calibrate( const void* context, const uint8_t calib_param ); + +/*! + * @brief Configure the regulator mode to be used in specific modes + * + * This function shall only be called in standby RC mode. + * + * The reg_mode parameter defines if the DC-DC converter is switched on in the following modes: STANDBY XOSC, FS, RX, TX + * and RX_CAPTURE. + * + * @param [in] context Chip implementation context + * @param [in] reg_mode Regulator mode configuration + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_set_reg_mode( const void* context, const lr11xx_system_reg_mode_t reg_mode ); + +/*! + * @brief Launch an image calibration valid for all frequencies inside an interval, in steps + * + * This function can be called in any mode of the chip. + * + * The chip will return to standby RC mode on exit. Potential calibration issues can be read out with + * lr11xx_system_get_errors command. + * + * The frequencies given in parameters are defined in 4MHz step (Eg. 900MHz corresponds to 0xE1). If freq1 = freq2, only + * one calibration is performed. + * + * @param [in] context Chip implementation context + * @param [in] freq1 Image calibration interval lower bound, in steps + * @param [in] freq2 Image calibration interval upper bound, in steps + * + * @remark freq1 must be less than or equal to freq2 + * + * @returns Operation status + * + * @see lr11xx_system_get_errors + */ +lr11xx_status_t lr11xx_system_calibrate_image( const void* context, const uint8_t freq1, const uint8_t freq2 ); + +/*! + * @brief Launch an image calibration valid for all frequencies inside an interval, in MHz + * + * @remark This function relies on @ref lr11xx_system_calibrate_image + * + * @param [in] context Chip implementation context + * @param [in] freq1_in_mhz Image calibration interval lower bound, in MHz + * @param [in] freq2_in_mhz Image calibration interval upper bound, in MHz + * + * @remark freq1 must be less than or equal to freq2 + * + * @returns Operation status + * + * @see lr11xx_system_calibrate_image + */ +lr11xx_status_t lr11xx_system_calibrate_image_in_mhz( const void* context, const uint16_t freq1_in_mhz, + const uint16_t freq2_in_mhz ); + +/*! + * @brief Set the RF switch configurations for each RF setup + * + * This function shall only be called in standby RC mode. + * + * By default, no DIO is used to control a RF switch. All DIOs are set in High-Z mode. + * + * @param [in] context Chip implementation context + * @param [in] rf_switch_cfg Pointer to a structure that holds the switches configuration + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_set_dio_as_rf_switch( const void* context, + const lr11xx_system_rfswitch_cfg_t* rf_switch_cfg ); + +/*! + * @brief Set which interrupt signals are redirected to the dedicated DIO pin + * + * By default, no interrupt signal is redirected. + * + * The dedicated DIO pin will remain asserted until all redirected interrupt signals are cleared with a call to + * lr11xx_system_clear_irq_status. + * + * @param [in] context Chip implementation context + * @param [in] irqs_to_enable_dio1 Variable that holds the interrupt mask for dio1 + * @param [in] irqs_to_enable_dio2 Variable that holds the interrupt mask for dio2 + * + * @returns Operation status + * + * @see lr11xx_system_clear_irq_status + */ +lr11xx_status_t lr11xx_system_set_dio_irq_params( const void* context, + const lr11xx_system_irq_mask_t irqs_to_enable_dio1, + const lr11xx_system_irq_mask_t irqs_to_enable_dio2 ); + +/*! + * @brief Clear requested bits in the internal pending interrupt register + * + * @param [in] context Chip implementation context + * @param [in] irqs_to_clear Variable that holds the interrupts to be cleared + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_clear_irq_status( const void* context, const lr11xx_system_irq_mask_t irqs_to_clear ); + +/** + * @brief This helper function clears any radio irq status flags that are set and returns the flags that were cleared. + * + * @param [in] context Chip implementation context. + * @param [out] irq Pointer to a variable for holding the system interrupt status. Can be NULL. + * + * @returns Operation status + * + * @see lr11xx_system_get_irq_status, lr11xx_system_clear_irq_status + */ +lr11xx_status_t lr11xx_system_get_and_clear_irq_status( const void* context, lr11xx_system_irq_mask_t* irq ); + +/*! + * @brief Defines which clock is used as Low Frequency (LF) clock + * + * @param [in] context Chip implementation context + * @param [in] lfclock_cfg Low frequency clock configuration + * @param [in] wait_for_32k_ready Tells the radio if it has to check if 32k source is ready before driving busy low + * + * @returns Operation status + * + * @see lr11xx_system_calibrate, lr11xx_system_calibrate_image + */ +lr11xx_status_t lr11xx_system_cfg_lfclk( const void* context, const lr11xx_system_lfclk_cfg_t lfclock_cfg, + const bool wait_for_32k_ready ); + +/*! + * @brief Enable and configure TCXO supply voltage and detection timeout + * + * This function shall only be called in standby RC mode. + * + * The timeout parameter is the maximum time the firmware waits for the TCXO to be ready. The timeout duration is given + * by: \f$ timeout\_duration\_us = timeout \times 30.52 \f$ + * + * The TCXO mode can be disabled by setting timeout parameter to 0. + * + * @param [in] context Chip implementation context + * @param [in] tune Supply voltage value + * @param [in] timeout Gating time before which the radio starts its Rx / Tx operation + * + * @returns Operation status + * + * @see lr11xx_system_calibrate, lr11xx_system_calibrate_image + */ +lr11xx_status_t lr11xx_system_set_tcxo_mode( const void* context, const lr11xx_system_tcxo_supply_voltage_t tune, + const uint32_t timeout ); + +/*! + * @brief Software reset of the chip. + * + * This function should be used to reboot the chip in a specified mode. Rebooting in flash mode presumes that the + * content in flash memory is not corrupted (i.e. the integrity check performed by the bootloader before executing the + * first instruction in flash is OK). + * + * @param [in] context Chip implementation context + * @param [in] stay_in_bootloader Selector to stay in bootloader or execute flash code after reboot. If true, the + * bootloader will not execute the flash code but activate SPI interface to allow firmware upgrade + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_reboot( const void* context, const bool stay_in_bootloader ); + +/*! + * @brief Returns the value of Vbat + * + * Vbat value (in V) is a function of Vana (typ. 1.35V) using the following + * formula: \f$ Vbat_{V} = (5 \times \frac{Vbat}{255} - 1) \times Vana \f$ + * + * @param [in] context Chip implementation context + * @param [out] vbat A pointer to the Vbat value + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_get_vbat( const void* context, uint8_t* vbat ); + +/*! + * @brief Returns the value of Temp + * + * The temperature (in °C) is a function of Vana (typ. 1.35V), Vbe25 (Vbe voltage @ 25°C, typ. 0.7295V) and VbeSlope + * (typ. -1.7mV/°C) using the following formula: + * \f$ Temperature_{°C} = (\frac{Temp(10:0)}{2047} \times Vana - Vbe25) \times \frac{1000}{VbeSlope} + 25 \f$ + * + * @remark If a TCXO is used, make sure to configure it with @ref lr11xx_system_set_tcxo_mode before calling this + * function + * + * @param [in] context Chip implementation context + * @param [out] temp A pointer to the Temp value + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_get_temp( const void* context, uint16_t* temp ); + +/*! + * @brief Set the device into Sleep or Deep Sleep Mode + * + * The sleep_cfg parameter defines in which sleep mode the device is put and if it wakes up after a given time on the + * RTC event. + * + * The sleep_time parameter is taken into account only when RtcTimeout = 1. It sets the sleep time in number of clock + * cycles: \f$ sleep\_time\_ms = sleep_time \times \frac{1}{32.768} \f$ + * + * @param [in] context Chip implementation context + * @param [in] sleep_cfg Sleep mode configuration + * @param [in] sleep_time Value of the RTC timeout (if RtcTimeout = 1) + * + * @returns Operation status + * + * @see lr11xx_system_set_standby, lr11xx_system_set_fs + */ +lr11xx_status_t lr11xx_system_set_sleep( const void* context, const lr11xx_system_sleep_cfg_t sleep_cfg, + const uint32_t sleep_time ); + +/*! + * @brief Set the device into the requested Standby mode + * + * @param [in] context Chip implementation context + * @param [in] standby_cfg Stand by mode configuration (RC or XOSC) + * + * @returns Operation status + * + * @see lr11xx_system_set_sleep, lr11xx_system_set_fs + */ +lr11xx_status_t lr11xx_system_set_standby( const void* context, const lr11xx_system_standby_cfg_t standby_cfg ); + +/*! + * @brief Set the device into Frequency Synthesis (FS) mode + * + * @param [in] context Chip implementation context + * + * @returns Operation status + * + * @see lr11xx_system_set_standby, lr11xx_system_set_sleep + */ +lr11xx_status_t lr11xx_system_set_fs( const void* context ); + +/*! + * @brief Erase an info page + * + * @param [in] context Chip implementation context + * @param [in] info_page_id Info page to be erased. Only LR11XX_SYSTEM_INFOPAGE_1 is allowed. + * + * @returns Operation status + * + * @see lr11xx_system_write_infopage, lr11xx_system_read_infopage + */ +lr11xx_status_t lr11xx_system_erase_infopage( const void* context, const lr11xx_system_infopage_id_t info_page_id ); + +/*! + * @brief Write data in an info page + * + * @param [in] context Chip implementation context + * @param [in] info_page_id Info page where data are written. Only LR11XX_SYSTEM_INFOPAGE_1 is allowed. + * @param [in] address Address within the info page (aligned on 32-bit data) + * @param [in] data Pointer to the data to write (data buffer shall be - at least - length words long) + * @param [in] length Number of 32-bit data to write (maximum value is 64) + * + * @returns Operation status + * + * @see lr11xx_system_erase_infopage, lr11xx_system_read_infopage + */ +lr11xx_status_t lr11xx_system_write_infopage( const void* context, const lr11xx_system_infopage_id_t info_page_id, + const uint16_t address, const uint32_t* data, const uint8_t length ); + +/*! + * @brief Read data from an info page + * + * It is possible to cross from page 0 to 1 if (address + length >= 512) + * + * @param [in] context Chip implementation context + * @param [in] info_page_id Info page where data are read + * @param [in] address Address within the info page (aligned on 32-bit data) + * @param [out] data Pointer to the data to read (data buffer shall be - at least - length words long) + * @param [in] length Number of 32-bit data to read (maximum value is 64) + * + * @returns Operation status + * + * @see lr11xx_system_erase_infopage, lr11xx_system_write_infopage + */ +lr11xx_status_t lr11xx_system_read_infopage( const void* context, const lr11xx_system_infopage_id_t info_page_id, + const uint16_t address, uint32_t* data, const uint8_t length ); + +/*! + * @brief Read and return the Unique Identifier of the LR11XX + * + * @param [in] context Chip implementation context + * @param [out] unique_identifier The buffer to be filled with the Unique Identifier of the LR11XX. It is up to the + * application to ensure unique_identifier is long enough to hold the unique identifier + * + * @returns Operation status + * + * @see LR11XX_SYSTEM_UID_LENGTH + */ +lr11xx_status_t lr11xx_system_read_uid( const void* context, lr11xx_system_uid_t unique_identifier ); + +/*! + * @brief Read and return the Join EUI of the LR11XX + * + * @param [in] context Chip implementation context + * @param [out] join_eui The buffer to be filled with Join EUI of the LR11XX. It is up to the application to ensure + * join_eui is long enough to hold the join EUI + * + * @returns Operation status + * + * @see LR11XX_SYSTEM_JOIN_EUI_LENGTH + */ +lr11xx_status_t lr11xx_system_read_join_eui( const void* context, lr11xx_system_join_eui_t join_eui ); + +/*! + * @brief Compute and return the PIN of the LR11XX based on factory default EUIs + * + * @remark Calling this command also triggers a derivation of network and application keys (available as @ref + * LR11XX_CRYPTO_KEYS_IDX_NWK_KEY and @ref LR11XX_CRYPTO_KEYS_IDX_APP_KEY) based on factory default EUIs + * + * @param [in] context Chip implementation context + * @param [out] pin The buffer to be filled with PIN of the LR11XX. It is up to the application to ensure pin is long + * enough to hold the PIN + * + * @returns Operation status + * + * @see LR11XX_SYSTEM_PIN_LENGTH + */ +lr11xx_status_t lr11xx_system_read_pin( const void* context, lr11xx_system_pin_t pin ); + +/*! + * @brief Compute and return the PIN of the LR11XX based on EUIs provided as parameters + * + * @remark Calling this command also triggers a derivation of network and application keys (available as @ref + * LR11XX_CRYPTO_KEYS_IDX_NWK_KEY and @ref LR11XX_CRYPTO_KEYS_IDX_APP_KEY) based on EUIs provided as parameters + * + * @param [in] context Chip implementation context + * @param [in] device_eui Custom Device EUI + * @param [in] join_eui Custom Join EUI + * @param [in] rfu Parameter RFU - shall be set to 0x00 + * @param [out] pin The buffer to be filled with PIN of the LR11XX. It is up to the application to ensure pin is long + * enough to hold the PIN + * + * @returns Operation status + * + * @see LR11XX_SYSTEM_PIN_LENGTH + */ +lr11xx_status_t lr11xx_system_read_pin_custom_eui( const void* context, lr11xx_system_uid_t device_eui, + lr11xx_system_join_eui_t join_eui, uint8_t rfu, + lr11xx_system_pin_t pin ); + +/*! + * @brief Read and return a 32-bit random number + * + * @remark Radio operating mode must be set into standby. + * + * @param [in] context Chip implementation context + * @param [out] random_number 32-bit random number + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_get_random_number( const void* context, uint32_t* random_number ); + +/*! + * @brief Enable the CRC on SPI transactions + * + * @remark This command shall always be sent with a CRC (to both enable and disable the feature). The function does not + * take care of this additional byte - which is under the responsibility of the underlying HAL functions + * + * @param [in] context Chip implementation context + * @param [in] enable_crc CRC + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_enable_spi_crc( const void* context, bool enable_crc ); + +/*! + * @brief Configure the GPIO drive in sleep mode + * + * @remark GPIO stands for RF switch and IRQ line DIOs + * + * @note This command is available from firmware version 0x0306 + * + * @param [in] context Chip implementation context + * @param [in] enable_drive GPIO drive configuration (true: enabled / false: disabled) + * + * @returns Operation status + */ +lr11xx_status_t lr11xx_system_drive_dio_in_sleep_mode( const void* context, bool enable_drive ); + +#ifdef __cplusplus +} +#endif + +#endif // LR11XX_SYSTEM_H + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_system_types.h b/zephcore/adapters/radio/lr11xx/lr11xx_system_types.h new file mode 100644 index 0000000..23dec41 --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_system_types.h @@ -0,0 +1,330 @@ +/*! + * @file lr11xx_system_types.h + * + * @brief System driver types for LR11XX + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LR11XX_SYSTEM_TYPES_H +#define LR11XX_SYSTEM_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +#include +#include + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC MACROS ----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC CONSTANTS -------------------------------------------------------- + */ + +/*! + * @brief Length in byte of the LR11XX version blob + */ +#define LR11XX_SYSTEM_VERSION_LENGTH ( 4 ) + +/*! + * @brief Length of the LR11XX Unique Identifier in bytes + * + * The LR11XX Unique Identifiers is an 8 byte long buffer + */ +#define LR11XX_SYSTEM_UID_LENGTH ( 8 ) +#define LR11XX_SYSTEM_JOIN_EUI_LENGTH ( 8 ) +#define LR11XX_SYSTEM_PIN_LENGTH ( 4 ) + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC TYPES ------------------------------------------------------------ + */ + +/** + * @brief Fixed-length array to store a UID + */ +typedef uint8_t lr11xx_system_uid_t[LR11XX_SYSTEM_UID_LENGTH]; + +/** + * @brief Fixed-length array to store a joinEUI + */ +typedef uint8_t lr11xx_system_join_eui_t[LR11XX_SYSTEM_JOIN_EUI_LENGTH]; + +/** + * @brief Fixed-length array to store a PIN + */ +typedef uint8_t lr11xx_system_pin_t[LR11XX_SYSTEM_PIN_LENGTH]; + +/** + * @brief Type to store system interrupt flags + */ +typedef uint32_t lr11xx_system_irq_mask_t; + +/** + * @brief Interrupt flags + */ +enum lr11xx_system_irq_e +{ + LR11XX_SYSTEM_IRQ_NONE = ( 0 << 0 ), + LR11XX_SYSTEM_IRQ_TX_DONE = ( 1 << 2 ), + LR11XX_SYSTEM_IRQ_RX_DONE = ( 1 << 3 ), + LR11XX_SYSTEM_IRQ_PREAMBLE_DETECTED = ( 1 << 4 ), + LR11XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID = ( 1 << 5 ), + LR11XX_SYSTEM_IRQ_HEADER_ERROR = ( 1 << 6 ), + LR11XX_SYSTEM_IRQ_CRC_ERROR = ( 1 << 7 ), + LR11XX_SYSTEM_IRQ_CAD_DONE = ( 1 << 8 ), + LR11XX_SYSTEM_IRQ_CAD_DETECTED = ( 1 << 9 ), + LR11XX_SYSTEM_IRQ_TIMEOUT = ( 1 << 10 ), + LR11XX_SYSTEM_IRQ_LR_FHSS_INTRA_PKT_HOP = ( 1 << 11 ), + LR11XX_SYSTEM_IRQ_GNSS_SCAN_DONE = ( 1 << 19 ), + LR11XX_SYSTEM_IRQ_WIFI_SCAN_DONE = ( 1 << 20 ), + LR11XX_SYSTEM_IRQ_EOL = ( 1 << 21 ), + LR11XX_SYSTEM_IRQ_CMD_ERROR = ( 1 << 22 ), + LR11XX_SYSTEM_IRQ_ERROR = ( 1 << 23 ), + LR11XX_SYSTEM_IRQ_FSK_LEN_ERROR = ( 1 << 24 ), + LR11XX_SYSTEM_IRQ_FSK_ADDR_ERROR = ( 1 << 25 ), + LR11XX_SYSTEM_IRQ_ALL_MASK = + LR11XX_SYSTEM_IRQ_TX_DONE | LR11XX_SYSTEM_IRQ_RX_DONE | LR11XX_SYSTEM_IRQ_PREAMBLE_DETECTED | + LR11XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID | LR11XX_SYSTEM_IRQ_HEADER_ERROR | LR11XX_SYSTEM_IRQ_CRC_ERROR | + LR11XX_SYSTEM_IRQ_CAD_DONE | LR11XX_SYSTEM_IRQ_CAD_DETECTED | LR11XX_SYSTEM_IRQ_TIMEOUT | + LR11XX_SYSTEM_IRQ_LR_FHSS_INTRA_PKT_HOP | LR11XX_SYSTEM_IRQ_GNSS_SCAN_DONE | LR11XX_SYSTEM_IRQ_WIFI_SCAN_DONE | + LR11XX_SYSTEM_IRQ_EOL | LR11XX_SYSTEM_IRQ_CMD_ERROR | LR11XX_SYSTEM_IRQ_ERROR | + LR11XX_SYSTEM_IRQ_FSK_LEN_ERROR | LR11XX_SYSTEM_IRQ_FSK_ADDR_ERROR, +}; + +/** + * @brief Calibration flags + */ +enum lr11xx_system_calibration_e +{ + LR11XX_SYSTEM_CALIB_LF_RC_MASK = ( 1 << 0 ), + LR11XX_SYSTEM_CALIB_HF_RC_MASK = ( 1 << 1 ), + LR11XX_SYSTEM_CALIB_PLL_MASK = ( 1 << 2 ), + LR11XX_SYSTEM_CALIB_ADC_MASK = ( 1 << 3 ), + LR11XX_SYSTEM_CALIB_IMG_MASK = ( 1 << 4 ), + LR11XX_SYSTEM_CALIB_PLL_TX_MASK = ( 1 << 5 ), +}; + +typedef uint8_t lr11xx_system_cal_mask_t; + +/** + * @brief Error flags + */ +enum lr11xx_system_errors_e +{ + LR11XX_SYSTEM_ERRORS_LF_RC_CALIB_MASK = ( 1 << 0 ), + LR11XX_SYSTEM_ERRORS_HF_RC_CALIB_MASK = ( 1 << 1 ), + LR11XX_SYSTEM_ERRORS_ADC_CALIB_MASK = ( 1 << 2 ), + LR11XX_SYSTEM_ERRORS_PLL_CALIB_MASK = ( 1 << 3 ), + LR11XX_SYSTEM_ERRORS_IMG_CALIB_MASK = ( 1 << 4 ), + LR11XX_SYSTEM_ERRORS_HF_XOSC_START_MASK = ( 1 << 5 ), + LR11XX_SYSTEM_ERRORS_LF_XOSC_START_MASK = ( 1 << 6 ), + LR11XX_SYSTEM_ERRORS_PLL_LOCK_MASK = ( 1 << 7 ), +}; + +typedef uint16_t lr11xx_system_errors_t; + +/** + * @brief Chip modes + */ +typedef enum +{ + LR11XX_SYSTEM_CHIP_MODE_SLEEP = 0x00, + LR11XX_SYSTEM_CHIP_MODE_STBY_RC = 0x01, + LR11XX_SYSTEM_CHIP_MODE_STBY_XOSC = 0x02, + LR11XX_SYSTEM_CHIP_MODE_FS = 0x03, + LR11XX_SYSTEM_CHIP_MODE_RX = 0x04, + LR11XX_SYSTEM_CHIP_MODE_TX = 0x05, + LR11XX_SYSTEM_CHIP_MODE_LOC = 0x06, +} lr11xx_system_chip_modes_t; + +/** + * @brief Reset status + */ +typedef enum +{ + LR11XX_SYSTEM_RESET_STATUS_CLEARED = 0x00, + LR11XX_SYSTEM_RESET_STATUS_ANALOG = 0x01, + LR11XX_SYSTEM_RESET_STATUS_EXTERNAL = 0x02, + LR11XX_SYSTEM_RESET_STATUS_SYSTEM = 0x03, + LR11XX_SYSTEM_RESET_STATUS_WATCHDOG = 0x04, + LR11XX_SYSTEM_RESET_STATUS_IOCD_RESTART = 0x05, + LR11XX_SYSTEM_RESET_STATUS_RTC_RESTART = 0x06, +} lr11xx_system_reset_status_t; + +/** + * @brief Command status + */ +typedef enum +{ + LR11XX_SYSTEM_CMD_STATUS_FAIL = 0x00, + LR11XX_SYSTEM_CMD_STATUS_PERR = 0x01, + LR11XX_SYSTEM_CMD_STATUS_OK = 0x02, + LR11XX_SYSTEM_CMD_STATUS_DATA = 0x03, +} lr11xx_system_command_status_t; + +/** + * @brief Low-frequency clock modes + */ +typedef enum +{ + LR11XX_SYSTEM_LFCLK_RC = 0x00, //!< (Default) + LR11XX_SYSTEM_LFCLK_XTAL = 0x01, + LR11XX_SYSTEM_LFCLK_EXT = 0x02 +} lr11xx_system_lfclk_cfg_t; + +/** + * @brief Regulator modes + */ +typedef enum +{ + LR11XX_SYSTEM_REG_MODE_LDO = 0x00, //!< (Default) + LR11XX_SYSTEM_REG_MODE_DCDC = 0x01, +} lr11xx_system_reg_mode_t; + +/** + * @brief Info page ID + */ +typedef enum +{ + LR11XX_SYSTEM_INFOPAGE_0 = 0x00, //!< Info page #0 + LR11XX_SYSTEM_INFOPAGE_1 = 0x01, //!< Info page #1 +} lr11xx_system_infopage_id_t; + +/** + * @brief RF switch configuration pin + */ +enum lr11xx_system_rfswitch_cfg_pin_e +{ + LR11XX_SYSTEM_RFSW0_HIGH = ( 1 << 0 ), + LR11XX_SYSTEM_RFSW1_HIGH = ( 1 << 1 ), + LR11XX_SYSTEM_RFSW2_HIGH = ( 1 << 2 ), + LR11XX_SYSTEM_RFSW3_HIGH = ( 1 << 3 ), + LR11XX_SYSTEM_RFSW4_HIGH = ( 1 << 4 ), +}; + +/** + * @brief RF switch configuration structure definition + */ +typedef struct lr11xx_system_rfswitch_cfg_s +{ + uint8_t enable; + uint8_t standby; + uint8_t rx; + uint8_t tx; + uint8_t tx_hp; + uint8_t tx_hf; + uint8_t gnss; + uint8_t wifi; +} lr11xx_system_rfswitch_cfg_t; + +/** + * @brief Stand by configuration values + */ +typedef enum +{ + LR11XX_SYSTEM_STANDBY_CFG_RC = 0x00, + LR11XX_SYSTEM_STANDBY_CFG_XOSC = 0x01 +} lr11xx_system_standby_cfg_t; + +/** + * @brief TCXO supply voltage values + */ +typedef enum +{ + LR11XX_SYSTEM_TCXO_CTRL_1_6V = 0x00, //!< Supply voltage = 1.6v + LR11XX_SYSTEM_TCXO_CTRL_1_7V = 0x01, //!< Supply voltage = 1.7v + LR11XX_SYSTEM_TCXO_CTRL_1_8V = 0x02, //!< Supply voltage = 1.8v + LR11XX_SYSTEM_TCXO_CTRL_2_2V = 0x03, //!< Supply voltage = 2.2v + LR11XX_SYSTEM_TCXO_CTRL_2_4V = 0x04, //!< Supply voltage = 2.4v + LR11XX_SYSTEM_TCXO_CTRL_2_7V = 0x05, //!< Supply voltage = 2.7v + LR11XX_SYSTEM_TCXO_CTRL_3_0V = 0x06, //!< Supply voltage = 3.0v + LR11XX_SYSTEM_TCXO_CTRL_3_3V = 0x07, //!< Supply voltage = 3.3v +} lr11xx_system_tcxo_supply_voltage_t; + +/** + * @brief Status register 1 structure definition + */ +typedef struct lr11xx_system_stat1_s +{ + lr11xx_system_command_status_t command_status; + bool is_interrupt_active; +} lr11xx_system_stat1_t; + +/** + * @brief Status register 2 structure definition + */ +typedef struct lr11xx_system_stat2_s +{ + lr11xx_system_reset_status_t reset_status; + lr11xx_system_chip_modes_t chip_mode; + bool is_running_from_flash; +} lr11xx_system_stat2_t; + +/** + * @brief Version structure definition + */ +typedef struct lr11xx_system_version_s +{ + uint8_t hw; + uint8_t type; + uint16_t fw; +} lr11xx_system_version_t; + +/** + * @brief Sleep configuration structure definition + */ +typedef struct lr11xx_system_sleep_cfg_s +{ + bool is_warm_start; + bool is_rtc_timeout; +} lr11xx_system_sleep_cfg_t; + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS PROTOTYPES --------------------------------------------- + */ + +#ifdef __cplusplus +} +#endif + +#endif // LR11XX_SYSTEM_TYPES_H + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_types.h b/zephcore/adapters/radio/lr11xx/lr11xx_types.h new file mode 100644 index 0000000..be52d7a --- /dev/null +++ b/zephcore/adapters/radio/lr11xx/lr11xx_types.h @@ -0,0 +1,76 @@ +/*! + * @file lr11xx_types.h + * + * @brief Type definitions for LR11XX + * + * The Clear BSD License + * Copyright Semtech Corporation 2021. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Semtech corporation nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY + * THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LR11XX_TYPES_H +#define LR11XX_TYPES_H + +/* + * ----------------------------------------------------------------------------- + * --- DEPENDENCIES ------------------------------------------------------------ + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC MACROS ----------------------------------------------------------- + */ + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC CONSTANTS -------------------------------------------------------- + */ + +#define LR11XX_CMD_LENGTH_MAX ( 512 ) + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC TYPES ------------------------------------------------------------ + */ + +/** + * @brief LR11XX status + */ +typedef enum lr11xx_status_e +{ + LR11XX_STATUS_OK = 0, + LR11XX_STATUS_ERROR = 3, +} lr11xx_status_t; + +/* + * ----------------------------------------------------------------------------- + * --- PUBLIC FUNCTIONS PROTOTYPES --------------------------------------------- + */ + +#endif // LR11XX_TYPES_H + +/* --- EOF ------------------------------------------------------------------ */ diff --git a/zephcore/adapters/radio/radio_common.h b/zephcore/adapters/radio/radio_common.h new file mode 100644 index 0000000..2446c44 --- /dev/null +++ b/zephcore/adapters/radio/radio_common.h @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Shared constants and utilities for all LoRa radio adapters. + * + * Anything duplicated between SX126xRadio and LR1110Radio belongs here. + * Radio-specific constants (e.g. SX126x duty cycle math) stay in + * their respective headers. + */ + +#pragma once + +#include + +/* --- Noise floor calibration --- */ +#define NUM_NOISE_FLOOR_SAMPLES 64 +#define NOISE_FLOOR_SAMPLING_THRESHOLD 14 /* only sample if rssi < floor + threshold */ +#define DEFAULT_NOISE_FLOOR 0 /* first calibration accepts all samples */ + +/* --- RX ring buffer --- */ +#define RX_RING_SIZE 4 + +/* --- TX wait thread --- */ +#define TX_WAIT_THREAD_STACK_SIZE 1024 +#define TX_WAIT_THREAD_PRIORITY 10 /* preemptible, lower than main */ +#define TX_TIMEOUT_MS 5000 /* TX completion timeout */ + +/* --- SNR thresholds per spreading factor (SF7..SF12) --- */ +inline constexpr float lora_snr_threshold[] = { + -7.5f, -10.0f, -12.5f, -15.0f, -17.5f, -20.0f +}; + +/* --- Callback types (ISR-safe) --- */ +typedef void (*RadioRxCallback)(void *user_data); +typedef void (*RadioTxDoneCallback)(void *user_data); + +/* --- Zephyr enum mapping utilities --- */ + +/* Map kHz bandwidth value to Zephyr enum. + * Input is (uint16_t)(float_bw) — truncated, e.g. 7.8→7, 10.4→10, 62.5→62. + * Zephyr >=4.4 has narrow BWs (7-62 kHz); <=4.3 only has 125/250/500. */ +static inline enum lora_signal_bandwidth bw_khz_to_enum(uint16_t bw_khz) +{ + switch (bw_khz) { + case 7: return BW_7_KHZ; + case 10: return BW_10_KHZ; + case 15: return BW_15_KHZ; + case 20: return BW_20_KHZ; + case 31: return BW_31_KHZ; + case 41: return BW_41_KHZ; + case 62: return BW_62_KHZ; + case 125: return BW_125_KHZ; + case 250: return BW_250_KHZ; + case 500: return BW_500_KHZ; + default: return BW_125_KHZ; + } +} + +/* Map ZephCore CR (5-8) to Zephyr coding_rate enum (1-4) */ +static inline enum lora_coding_rate cr_to_enum(uint8_t cr) +{ + switch (cr) { + case 5: return CR_4_5; + case 6: return CR_4_6; + case 7: return CR_4_7; + case 8: return CR_4_8; + default: return CR_4_5; + } +} diff --git a/zephcore/adapters/rng/RNG.cpp b/zephcore/adapters/rng/RNG.cpp new file mode 100644 index 0000000..6903399 --- /dev/null +++ b/zephcore/adapters/rng/RNG.cpp @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * RNG::nextInt - shared implementation + */ + +#include + +namespace mesh { + +uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) +{ + uint32_t num; + random((uint8_t *)&num, sizeof(num)); + if (_max <= _min) { + return _min; + } + return (num % (_max - _min)) + _min; +} + +} /* namespace mesh */ diff --git a/zephcore/adapters/rng/ZephyrRNG.cpp b/zephcore/adapters/rng/ZephyrRNG.cpp new file mode 100644 index 0000000..372c225 --- /dev/null +++ b/zephcore/adapters/rng/ZephyrRNG.cpp @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ZephyrRNG.h" +#include +#include + +namespace mesh { + +void ZephyrRNG::random(uint8_t *dest, size_t sz) +{ + int ret = sys_csrand_get(dest, sz); + if (ret != 0) { + printk("ZephyrRNG: CSPRNG failed (%d), using PRNG fallback\n", ret); + sys_rand_get(dest, sz); + } +} + +} /* namespace mesh */ diff --git a/zephcore/adapters/rng/ZephyrRNG.h b/zephcore/adapters/rng/ZephyrRNG.h new file mode 100644 index 0000000..ffc7962 --- /dev/null +++ b/zephcore/adapters/rng/ZephyrRNG.h @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Zephyr CSPRNG implementation + */ + +#pragma once + +#include + +namespace mesh { + +class ZephyrRNG : public RNG { +public: + void random(uint8_t *dest, size_t sz) override; +}; + +} /* namespace mesh */ diff --git a/zephcore/adapters/sensors/SimpleLPP.h b/zephcore/adapters/sensors/SimpleLPP.h new file mode 100644 index 0000000..771da6a --- /dev/null +++ b/zephcore/adapters/sensors/SimpleLPP.h @@ -0,0 +1,257 @@ +/* + * SimpleLPP - Minimal CayenneLPP encoder for Zephyr + * + * Implements a subset of CayenneLPP format for telemetry encoding. + * Supports the sensor types used in ZephCore's EnvironmentSensorManager. + * + * Format: [channel:1][type:1][value:N bytes big-endian] + * + * SPDX-License-Identifier: MIT + */ + +#ifndef SIMPLE_LPP_H +#define SIMPLE_LPP_H + +#include +#include +#include + +/* LPP Type Codes (from CayenneLPP spec) */ +#define LPP_ANALOG_INPUT 2 /* 2 bytes, 0.01 signed */ +#define LPP_TEMPERATURE 103 /* 2 bytes, 0.1°C signed */ +#define LPP_RELATIVE_HUMIDITY 104 /* 1 byte, 0.5% unsigned */ +#define LPP_BAROMETRIC_PRESSURE 115 /* 2 bytes, 0.1 hPa unsigned */ +#define LPP_VOLTAGE 116 /* 2 bytes, 0.01V unsigned */ +#define LPP_CURRENT 117 /* 2 bytes, 0.001A unsigned */ +#define LPP_ALTITUDE 121 /* 2 bytes, 1m signed */ +#define LPP_POWER 128 /* 2 bytes, 1W unsigned */ +#define LPP_DISTANCE 130 /* 4 bytes, 0.001m unsigned */ +#define LPP_GPS 136 /* 9 bytes: 3 lat, 3 lon, 3 alt */ + +/* Multipliers */ +#define LPP_TEMPERATURE_MULT 10 +#define LPP_HUMIDITY_MULT 2 +#define LPP_PRESSURE_MULT 10 +#define LPP_VOLTAGE_MULT 100 +#define LPP_CURRENT_MULT 1000 +#define LPP_DISTANCE_MULT 1000 +#define LPP_ANALOG_MULT 100 +#define LPP_GPS_LAT_LON_MULT 10000 +#define LPP_GPS_ALT_MULT 100 + +/** + * SimpleLPP - Minimal CayenneLPP encoder + * + * Usage: + * uint8_t buffer[64]; + * SimpleLPP lpp(buffer, sizeof(buffer)); + * lpp.addVoltage(0, 3.85f); // Battery voltage + * lpp.addTemperature(0, 25.5f); // Temperature + * // Use lpp.getBuffer() and lpp.getSize() to get encoded data + */ +class SimpleLPP { +public: + /** + * Constructor + * @param buffer Buffer to write LPP data into + * @param max_size Maximum buffer size + */ + SimpleLPP(uint8_t *buffer, size_t max_size) + : _buffer(buffer), _maxsize(max_size), _cursor(0) {} + + /** Reset the buffer cursor for reuse */ + void reset() { _cursor = 0; } + + /** Get current encoded data size */ + size_t getSize() const { return _cursor; } + + /** Get pointer to buffer */ + uint8_t *getBuffer() { return _buffer; } + + /** Get const pointer to buffer */ + const uint8_t *getBuffer() const { return _buffer; } + + /** + * Add temperature reading + * @param channel Channel number (0 = self) + * @param celsius Temperature in degrees Celsius + * @return Number of bytes written, or 0 on overflow + */ + uint8_t addTemperature(uint8_t channel, float celsius) { + int16_t val = (int16_t)(celsius * LPP_TEMPERATURE_MULT); + return addField2Signed(channel, LPP_TEMPERATURE, val); + } + + /** + * Add relative humidity reading + * @param channel Channel number + * @param percent Humidity in percent (0-100) + * @return Number of bytes written, or 0 on overflow + */ + uint8_t addRelativeHumidity(uint8_t channel, float percent) { + uint8_t val = (uint8_t)(percent * LPP_HUMIDITY_MULT); + return addField1(channel, LPP_RELATIVE_HUMIDITY, val); + } + + /** + * Add barometric pressure reading + * @param channel Channel number + * @param hpa Pressure in hectopascals (hPa / mbar) + * @return Number of bytes written, or 0 on overflow + */ + uint8_t addBarometricPressure(uint8_t channel, float hpa) { + uint16_t val = (uint16_t)(hpa * LPP_PRESSURE_MULT); + return addField2Unsigned(channel, LPP_BAROMETRIC_PRESSURE, val); + } + + /** + * Add voltage reading + * @param channel Channel number + * @param volts Voltage in volts + * @return Number of bytes written, or 0 on overflow + */ + uint8_t addVoltage(uint8_t channel, float volts) { + uint16_t val = (uint16_t)(volts * LPP_VOLTAGE_MULT); + return addField2Unsigned(channel, LPP_VOLTAGE, val); + } + + /** + * Add current reading + * @param channel Channel number + * @param amps Current in amperes + * @return Number of bytes written, or 0 on overflow + */ + uint8_t addCurrent(uint8_t channel, float amps) { + uint16_t val = (uint16_t)(amps * LPP_CURRENT_MULT); + return addField2Unsigned(channel, LPP_CURRENT, val); + } + + /** + * Add power reading + * @param channel Channel number + * @param watts Power in watts + * @return Number of bytes written, or 0 on overflow + */ + uint8_t addPower(uint8_t channel, float watts) { + uint16_t val = (uint16_t)watts; + return addField2Unsigned(channel, LPP_POWER, val); + } + + /** + * Add altitude reading + * @param channel Channel number + * @param meters Altitude in meters + * @return Number of bytes written, or 0 on overflow + */ + uint8_t addAltitude(uint8_t channel, float meters) { + int16_t val = (int16_t)meters; + return addField2Signed(channel, LPP_ALTITUDE, val); + } + + /** + * Add distance reading + * @param channel Channel number + * @param meters Distance in meters + * @return Number of bytes written, or 0 on overflow + */ + uint8_t addDistance(uint8_t channel, float meters) { + uint32_t val = (uint32_t)(meters * LPP_DISTANCE_MULT); + return addField4(channel, LPP_DISTANCE, val); + } + + /** + * Add analog input (generic sensor value) + * @param channel Channel number + * @param value Analog value (scaled by 0.01) + * @return Number of bytes written, or 0 on overflow + */ + uint8_t addAnalogInput(uint8_t channel, float value) { + int16_t val = (int16_t)(value * LPP_ANALOG_MULT); + return addField2Signed(channel, LPP_ANALOG_INPUT, val); + } + + /** + * Add GPS location + * @param channel Channel number + * @param latitude Latitude in degrees + * @param longitude Longitude in degrees + * @param altitude Altitude in meters + * @return Number of bytes written, or 0 on overflow + */ + uint8_t addGPS(uint8_t channel, float latitude, float longitude, float altitude) { + /* GPS format: 3 bytes lat, 3 bytes lon, 3 bytes alt (all signed, big-endian) */ + if (_cursor + 11 > _maxsize) return 0; + + int32_t lat = (int32_t)(latitude * LPP_GPS_LAT_LON_MULT); + int32_t lon = (int32_t)(longitude * LPP_GPS_LAT_LON_MULT); + int32_t alt = (int32_t)(altitude * LPP_GPS_ALT_MULT); + + _buffer[_cursor++] = channel; + _buffer[_cursor++] = LPP_GPS; + + /* Latitude - 3 bytes signed */ + _buffer[_cursor++] = (lat >> 16) & 0xFF; + _buffer[_cursor++] = (lat >> 8) & 0xFF; + _buffer[_cursor++] = lat & 0xFF; + + /* Longitude - 3 bytes signed */ + _buffer[_cursor++] = (lon >> 16) & 0xFF; + _buffer[_cursor++] = (lon >> 8) & 0xFF; + _buffer[_cursor++] = lon & 0xFF; + + /* Altitude - 3 bytes signed */ + _buffer[_cursor++] = (alt >> 16) & 0xFF; + _buffer[_cursor++] = (alt >> 8) & 0xFF; + _buffer[_cursor++] = alt & 0xFF; + + return 11; + } + +private: + uint8_t *_buffer; + size_t _maxsize; + size_t _cursor; + + /* Add 1-byte unsigned field */ + uint8_t addField1(uint8_t channel, uint8_t type, uint8_t value) { + if (_cursor + 3 > _maxsize) return 0; + _buffer[_cursor++] = channel; + _buffer[_cursor++] = type; + _buffer[_cursor++] = value; + return 3; + } + + /* Add 2-byte unsigned field (big-endian) */ + uint8_t addField2Unsigned(uint8_t channel, uint8_t type, uint16_t value) { + if (_cursor + 4 > _maxsize) return 0; + _buffer[_cursor++] = channel; + _buffer[_cursor++] = type; + _buffer[_cursor++] = (value >> 8) & 0xFF; + _buffer[_cursor++] = value & 0xFF; + return 4; + } + + /* Add 2-byte signed field (big-endian) */ + uint8_t addField2Signed(uint8_t channel, uint8_t type, int16_t value) { + if (_cursor + 4 > _maxsize) return 0; + _buffer[_cursor++] = channel; + _buffer[_cursor++] = type; + _buffer[_cursor++] = (value >> 8) & 0xFF; + _buffer[_cursor++] = value & 0xFF; + return 4; + } + + /* Add 4-byte unsigned field (big-endian) */ + uint8_t addField4(uint8_t channel, uint8_t type, uint32_t value) { + if (_cursor + 6 > _maxsize) return 0; + _buffer[_cursor++] = channel; + _buffer[_cursor++] = type; + _buffer[_cursor++] = (value >> 24) & 0xFF; + _buffer[_cursor++] = (value >> 16) & 0xFF; + _buffer[_cursor++] = (value >> 8) & 0xFF; + _buffer[_cursor++] = value & 0xFF; + return 6; + } +}; + +#endif /* SIMPLE_LPP_H */ diff --git a/zephcore/adapters/sensors/ZephyrEnvSensors.cpp b/zephcore/adapters/sensors/ZephyrEnvSensors.cpp new file mode 100644 index 0000000..f401b85 --- /dev/null +++ b/zephcore/adapters/sensors/ZephyrEnvSensors.cpp @@ -0,0 +1,384 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Zephyr Environment & Power Sensors + * + * Auto-detects available sensors via Zephyr devicetree nodelabels. + * + * Environment sensors (temp/humidity/pressure): + * SHTC3, AHT20/DHT20/AM2301B, SHT4x, SHT3xD, BME280, BME680, BMP280, BMP388, LPS22HB + * MCU die temperature as fallback (nordic,nrf-temp) + * + * Power monitors (voltage/current/power): + * INA219, INA3221, INA226, INA228, INA230, INA232, INA236, INA237 + */ + +#include "ZephyrEnvSensors.h" + +#include +#include +#include + +LOG_MODULE_REGISTER(zephcore_sensors, CONFIG_ZEPHCORE_SENSORS_LOG_LEVEL); + +/* ========== Sensor Support ========== */ +#if IS_ENABLED(CONFIG_SENSOR) +#define HAS_ENV_SENSORS 1 +#include +#else +#define HAS_ENV_SENSORS 0 +#endif + +/* INA3221 channel selection attribute — defined in driver's private header + * (zephyr/drivers/sensor/ti/ina3221/ina3221.h), replicated here to avoid + * including private driver internals */ +#define SENSOR_ATTR_INA3221_SELECTED_CHANNEL (SENSOR_ATTR_PRIV_START + 1) + +/* ================================================================ + * DEVICE_DT_GET_OR_NULL helper + * + * Returns a const struct device* for a DT nodelabel if it exists + * in the board's devicetree, or NULL at compile time if it doesn't. + * This is the proper Zephyr pattern — device_get_binding() is deprecated. + * ================================================================ */ + +/* ================================================================ + * Environment Sensors + * ================================================================ */ + +#if HAS_ENV_SENSORS +static const struct device *temp_humidity_dev = NULL; +static const struct device *pressure_dev = NULL; +static bool temp_dev_has_pressure = false; /* BME280/BME680 also have pressure */ +static bool env_available = false; +#endif + +int env_sensors_init(void) +{ +#if HAS_ENV_SENSORS + const struct device *dev; + + /* === Temperature/Humidity sensors === + * Priority order: dedicated temp/humidity first, then combo sensors. + * BME280/BME680 also provide pressure — tracked via temp_dev_has_pressure. */ + + /* SHTC3 (e.g., RAK1901) */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(shtc3)); + if (dev && device_is_ready(dev)) { + temp_humidity_dev = dev; + LOG_INF("Found temp/humidity sensor: %s (SHTC3)", dev->name); + goto check_pressure; + } + + /* Aosong AHT20/DHT20/AM2301B — same chip family, three compatible strings */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(aht20)); + if (!dev || !device_is_ready(dev)) { + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(dht20)); + } + if (!dev || !device_is_ready(dev)) { + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(am2301b)); + } + if (dev && device_is_ready(dev)) { + temp_humidity_dev = dev; + LOG_INF("Found temp/humidity sensor: %s (AHT20/DHT20)", dev->name); + goto check_pressure; + } + + /* SHT4x */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sht4x)); + if (dev && device_is_ready(dev)) { + temp_humidity_dev = dev; + LOG_INF("Found temp/humidity sensor: %s (SHT4x)", dev->name); + goto check_pressure; + } + + /* SHT3xD */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sht3xd)); + if (dev && device_is_ready(dev)) { + temp_humidity_dev = dev; + LOG_INF("Found temp/humidity sensor: %s (SHT3xD)", dev->name); + goto check_pressure; + } + + /* BME280 — temperature + humidity + pressure */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(bme280)); + if (dev && device_is_ready(dev)) { + temp_humidity_dev = dev; + temp_dev_has_pressure = true; + LOG_INF("Found env sensor: %s (BME280 — temp/humidity/pressure)", dev->name); + goto check_pressure; + } + + /* BME680 — temperature + humidity + pressure (+ gas) */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(bme680)); + if (dev && device_is_ready(dev)) { + temp_humidity_dev = dev; + temp_dev_has_pressure = true; + LOG_INF("Found env sensor: %s (BME680 — temp/humidity/pressure)", dev->name); + goto check_pressure; + } + +check_pressure: + /* === Pressure-only sensors === + * Only needed if the temp/humidity sensor doesn't provide pressure. */ + if (!temp_dev_has_pressure) { + /* LPS22HB (e.g., RAK1902) */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(lps22hb)); + if (dev && device_is_ready(dev)) { + pressure_dev = dev; + LOG_INF("Found pressure sensor: %s (LPS22HB)", dev->name); + goto done; + } + + /* BMP280 — pressure + temperature (lower priority as temp source) */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(bmp280)); + if (dev && device_is_ready(dev)) { + pressure_dev = dev; + LOG_INF("Found pressure sensor: %s (BMP280)", dev->name); + goto done; + } + + /* BMP388 */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(bmp388)); + if (dev && device_is_ready(dev)) { + pressure_dev = dev; + LOG_INF("Found pressure sensor: %s (BMP388)", dev->name); + goto done; + } + } + +done: + env_available = (temp_humidity_dev != NULL) || (pressure_dev != NULL); + if (!env_available) { + LOG_INF("No environment sensors found"); + } +#endif + + return 0; +} + +bool env_sensors_available(void) +{ +#if HAS_ENV_SENSORS + return env_available; +#else + return false; +#endif +} + +int env_sensors_read(struct env_data *data) +{ + memset(data, 0, sizeof(*data)); + +#if HAS_ENV_SENSORS + struct sensor_value val; + int rc; + + /* === Read temperature/humidity sensor === */ + if (temp_humidity_dev) { + rc = sensor_sample_fetch(temp_humidity_dev); + if (rc == 0) { + if (sensor_channel_get(temp_humidity_dev, SENSOR_CHAN_AMBIENT_TEMP, &val) == 0) { + data->temperature_c = sensor_value_to_float(&val); + data->has_temperature = true; + } + if (sensor_channel_get(temp_humidity_dev, SENSOR_CHAN_HUMIDITY, &val) == 0) { + data->humidity_pct = sensor_value_to_float(&val); + data->has_humidity = true; + } + /* BME280/BME680 also have pressure — read from same device */ + if (temp_dev_has_pressure) { + if (sensor_channel_get(temp_humidity_dev, SENSOR_CHAN_PRESS, &val) == 0) { + data->pressure_hpa = sensor_value_to_float(&val) * 10.0f; + data->has_pressure = true; + } + } + } + } + + /* === Read dedicated pressure sensor (if not already from combo sensor) === */ + if (pressure_dev && !data->has_pressure) { + if (pressure_dev != temp_humidity_dev) { + sensor_sample_fetch(pressure_dev); + } + if (sensor_channel_get(pressure_dev, SENSOR_CHAN_PRESS, &val) == 0) { + data->pressure_hpa = sensor_value_to_float(&val) * 10.0f; + data->has_pressure = true; + } + } + + /* === MCU die temperature — always read when available === + * Used as fallback when no external temp sensor, and always + * available via has_mcu_temperature for telemetry decisions. */ + const struct device *mcu_temp = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(temp)); + if (mcu_temp && device_is_ready(mcu_temp)) { + if (sensor_sample_fetch(mcu_temp) == 0 && + sensor_channel_get(mcu_temp, SENSOR_CHAN_DIE_TEMP, &val) == 0) { + data->mcu_temperature_c = sensor_value_to_float(&val); + data->has_mcu_temperature = true; + } + } + + return (data->has_temperature || data->has_humidity || data->has_pressure || + data->has_mcu_temperature) ? 0 : -ENODATA; +#else + return -ENOTSUP; +#endif +} + +/* ================================================================ + * Power Monitor Sensors (INA family) + * + * Three Zephyr driver families: + * - INA219 (ti,ina219) — standalone, 1 channel + * - INA3221 (ti,ina3221) — standalone, 3 channels with channel selection + * - ina2xx (ti,ina226/228/230/232/236/237) — unified driver, 1 channel + * + * All use: SENSOR_CHAN_VOLTAGE, SENSOR_CHAN_CURRENT, SENSOR_CHAN_POWER + * ================================================================ */ + +#if HAS_ENV_SENSORS + +enum ina_type { INA_NONE, INA_219, INA_3221, INA_2XX }; + +static const struct device *ina_dev = NULL; +static enum ina_type ina_found = INA_NONE; +static uint8_t ina_num_channels = 0; +static bool ina_available = false; + +#endif /* HAS_ENV_SENSORS */ + +int power_sensors_init(void) +{ +#if HAS_ENV_SENSORS + const struct device *dev; + + /* INA3221 — 3-channel power monitor (check first — most channels) */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina3221)); + if (dev && device_is_ready(dev)) { + ina_dev = dev; + ina_found = INA_3221; + ina_num_channels = 3; + ina_available = true; + LOG_INF("Found power monitor: %s (INA3221, 3 channels)", dev->name); + return 0; + } + + /* INA219 — standalone single-channel */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina219)); + if (dev && device_is_ready(dev)) { + ina_dev = dev; + ina_found = INA_219; + ina_num_channels = 1; + ina_available = true; + LOG_INF("Found power monitor: %s (INA219)", dev->name); + return 0; + } + + /* ina2xx unified family — try all supported variants */ + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina226)); + if (!dev || !device_is_ready(dev)) { + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina228)); + } + if (!dev || !device_is_ready(dev)) { + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina230)); + } + if (!dev || !device_is_ready(dev)) { + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina232)); + } + if (!dev || !device_is_ready(dev)) { + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina236)); + } + if (!dev || !device_is_ready(dev)) { + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ina237)); + } + if (dev && device_is_ready(dev)) { + ina_dev = dev; + ina_found = INA_2XX; + ina_num_channels = 1; + ina_available = true; + LOG_INF("Found power monitor: %s (ina2xx)", dev->name); + return 0; + } + + LOG_INF("No power monitors found"); +#endif + + return 0; +} + +bool power_sensors_available(void) +{ +#if HAS_ENV_SENSORS + return ina_available; +#else + return false; +#endif +} + +int power_sensors_read(struct power_data *data) +{ + memset(data, 0, sizeof(*data)); + +#if HAS_ENV_SENSORS + if (!ina_available || !ina_dev) { + return -ENODEV; + } + + struct sensor_value val; + + if (ina_found == INA_3221) { + /* INA3221: iterate channels 1-3, select each before reading */ + data->num_channels = ina_num_channels; + for (int ch = 0; ch < ina_num_channels; ch++) { + struct sensor_value sel; + sel.val1 = ch + 1; /* INA3221 channels are 1-indexed */ + sel.val2 = 0; + + int rc = sensor_attr_set(ina_dev, SENSOR_CHAN_ALL, + (enum sensor_attribute)SENSOR_ATTR_INA3221_SELECTED_CHANNEL, + &sel); + if (rc != 0) { + continue; + } + + rc = sensor_sample_fetch(ina_dev); + if (rc != 0) { + continue; + } + + if (sensor_channel_get(ina_dev, SENSOR_CHAN_VOLTAGE, &val) == 0) { + data->channels[ch].voltage_v = sensor_value_to_float(&val); + } + if (sensor_channel_get(ina_dev, SENSOR_CHAN_CURRENT, &val) == 0) { + data->channels[ch].current_a = sensor_value_to_float(&val); + } + if (sensor_channel_get(ina_dev, SENSOR_CHAN_POWER, &val) == 0) { + data->channels[ch].power_w = sensor_value_to_float(&val); + } + data->channels[ch].valid = true; + } + } else { + /* INA219 / ina2xx unified: single-channel, straightforward read */ + data->num_channels = 1; + int rc = sensor_sample_fetch(ina_dev); + if (rc != 0) { + return -EIO; + } + + if (sensor_channel_get(ina_dev, SENSOR_CHAN_VOLTAGE, &val) == 0) { + data->channels[0].voltage_v = sensor_value_to_float(&val); + } + if (sensor_channel_get(ina_dev, SENSOR_CHAN_CURRENT, &val) == 0) { + data->channels[0].current_a = sensor_value_to_float(&val); + } + if (sensor_channel_get(ina_dev, SENSOR_CHAN_POWER, &val) == 0) { + data->channels[0].power_w = sensor_value_to_float(&val); + } + data->channels[0].valid = true; + } + + return 0; +#else + return -ENOTSUP; +#endif +} diff --git a/zephcore/adapters/sensors/ZephyrEnvSensors.h b/zephcore/adapters/sensors/ZephyrEnvSensors.h new file mode 100644 index 0000000..bca2de8 --- /dev/null +++ b/zephcore/adapters/sensors/ZephyrEnvSensors.h @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Zephyr Environment & Power Sensors + * + * Environment: temperature, humidity, pressure + * Supports: SHTC3, AHT20/DHT20/AM2301B, SHT4x, SHT3x, BME280, BME680, BMP280, BMP388, LPS22HB + * MCU die temperature as fallback (nordic,nrf-temp) + * + * Power monitors: voltage, current, power + * Supports: INA219, INA3221, INA226, INA228, INA230, INA232, INA236, INA237 + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ========== Environment Sensors ========== */ + +/* Environment sensor data */ +struct env_data { + float temperature_c; /* Temperature in Celsius (external sensor) */ + float humidity_pct; /* Relative humidity in percent */ + float pressure_hpa; /* Barometric pressure in hPa */ + float mcu_temperature_c; /* MCU die temperature in Celsius */ + bool has_temperature; + bool has_humidity; + bool has_pressure; + bool has_mcu_temperature; +}; + +/* Initialize environment sensors (call once at boot) */ +int env_sensors_init(void); + +/* Environment sensor functions */ +bool env_sensors_available(void); +int env_sensors_read(struct env_data *data); + +/* ========== Power Monitor Sensors ========== */ + +#define POWER_MAX_CHANNELS 4 /* INA3221 has 3 channels + 1 spare */ + +/* Single power monitor channel */ +struct power_channel { + float voltage_v; /* Bus voltage in volts */ + float current_a; /* Current in amperes */ + float power_w; /* Power in watts */ + bool valid; /* True if this channel was read successfully */ +}; + +/* Power monitor data */ +struct power_data { + struct power_channel channels[POWER_MAX_CHANNELS]; + uint8_t num_channels; /* Number of valid channels */ +}; + +/* Initialize power monitor sensors (call once at boot) */ +int power_sensors_init(void); + +/* Power monitor functions */ +bool power_sensors_available(void); +int power_sensors_read(struct power_data *data); + +#ifdef __cplusplus +} +#endif diff --git a/zephcore/adapters/sensors/ZephyrSensorManager.h b/zephcore/adapters/sensors/ZephyrSensorManager.h new file mode 100644 index 0000000..4ca976b --- /dev/null +++ b/zephcore/adapters/sensors/ZephyrSensorManager.h @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Backward-compatibility shim — includes GPS manager + env sensors. + * All callers can continue using #include "ZephyrSensorManager.h". + */ + +#pragma once + +#include "ZephyrGPSManager.h" +#include "ZephyrEnvSensors.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Legacy combined init — calls all subsystem inits */ +static inline int sensor_manager_init(void) +{ + gps_manager_init(); + env_sensors_init(); + power_sensors_init(); + return 0; +} + +#ifdef __cplusplus +} +#endif diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp new file mode 100644 index 0000000..7111b6b --- /dev/null +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp @@ -0,0 +1,251 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore USB CDC Companion Transport + * + * V3-framed USB CDC for companion mode. Extracted from main_companion.cpp. + * Only compiled when CONFIG_LOG is enabled (debug builds). + */ + +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_usb, CONFIG_ZEPHCORE_USB_LOG_LEVEL); + +#include +#include +#include + +#include "ZephyrCompanionUSB.h" + +/* MAX_FRAME_SIZE defined in CompanionMesh.h */ + +#define USB_DTR_CHECK_MS 10000 /* USB DTR poll interval - 10s for power savings */ +#define USB_RING_BUF_SIZE 512 /* USB RX ring buffer size */ + +/* USB CDC state */ +static const struct device *usb_dev; +static uint8_t usb_ring_buf_data[USB_RING_BUF_SIZE]; +static struct ring_buf usb_ring_buf; +static uint8_t usb_rx_buf[MAX_FRAME_SIZE + 2]; /* +2 for length prefix */ +static uint16_t usb_rx_idx; +static uint16_t usb_frame_len; /* Expected frame length (0 = waiting for header) */ +static bool usb_dtr_active; + +/* Pointers to mesh event infrastructure (set by init) */ +static struct k_event *s_mesh_events; +static struct k_work *s_rx_work; +static uint32_t s_mesh_event_ble_rx; +static mesh::ZephyrBoard *s_board; + +/* Work items */ +static void usb_rx_work_fn(struct k_work *work); +static void usb_dtr_check_work_fn(struct k_work *work); + +K_WORK_DEFINE(usb_rx_work, usb_rx_work_fn); +K_WORK_DELAYABLE_DEFINE(usb_dtr_check_work, usb_dtr_check_work_fn); + +/** + * Reboot into the Adafruit nRF52 bootloader DFU mode. + * Triggered by host opening USB CDC at 1200 baud then dropping DTR. + */ +static void reboot_to_bootloader_dfu(void) +{ + LOG_INF("*** REBOOTING TO BOOTLOADER DFU ***"); + s_board->rebootToBootloader(); + CODE_UNREACHABLE; +} + +/* USB CDC UART interrupt callback - puts bytes in ring buffer */ +static void usb_uart_isr(const struct device *dev, void *user_data) +{ + ARG_UNUSED(user_data); + + while (uart_irq_update(dev) && uart_irq_is_pending(dev)) { + if (uart_irq_rx_ready(dev)) { + uint8_t buf[64]; + int recv_len = uart_fifo_read(dev, buf, sizeof(buf)); + if (recv_len > 0) { + ring_buf_put(&usb_ring_buf, buf, recv_len); + k_work_submit(&usb_rx_work); + } + } + } +} + +/* USB RX work - parses V3 frames from ring buffer */ +static void usb_rx_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + uint8_t byte; + + while (ring_buf_get(&usb_ring_buf, &byte, 1) == 1) { + /* V3 framing: [len_lo][len_hi][payload...] */ + if (usb_rx_idx == 0) { + /* First byte of length */ + usb_rx_buf[0] = byte; + usb_rx_idx = 1; + } else if (usb_rx_idx == 1) { + /* Second byte of length */ + usb_rx_buf[1] = byte; + usb_frame_len = usb_rx_buf[0] | (usb_rx_buf[1] << 8); + usb_rx_idx = 2; + + if (usb_frame_len == 0 || usb_frame_len > MAX_FRAME_SIZE) { + LOG_WRN("usb_rx: invalid frame len %u, resync", usb_frame_len); + usb_frame_len = 0; + usb_rx_idx = 0; + } + } else { + /* Payload bytes */ + usb_rx_buf[usb_rx_idx++] = byte; + + if (usb_rx_idx >= usb_frame_len + 2) { + /* Frame complete - queue it */ + uint8_t *payload = &usb_rx_buf[2]; + uint16_t payload_len = usb_frame_len; + + LOG_DBG("usb_rx: frame complete len=%u hdr=0x%02x", payload_len, payload[0]); + + /* Check for CMD_APP_START to switch interface */ + if (payload_len >= 1 && payload[0] == 0x03 /* CMD_APP_START */) { + if (zephcore_ble_get_active_iface() == ZEPHCORE_IFACE_BLE && + zephcore_ble_is_connected()) { + LOG_INF("usb_rx: CMD_APP_START, disconnecting BLE"); + zephcore_ble_disconnect(); + } + zephcore_ble_set_active_iface(ZEPHCORE_IFACE_USB); + LOG_INF("usb_rx: active_iface = IFACE_USB"); + } + + /* Only process if USB is active interface */ + if (zephcore_ble_get_active_iface() == ZEPHCORE_IFACE_USB) { + struct { + uint16_t len; + uint8_t buf[MAX_FRAME_SIZE]; + } f; + f.len = payload_len; + memcpy(f.buf, payload, payload_len); + if (k_msgq_put(zephcore_ble_get_recv_queue(), &f, K_NO_WAIT) == 0) { + k_work_submit(s_rx_work); + k_event_post(s_mesh_events, s_mesh_event_ble_rx); + } + } + + /* Reset for next frame */ + usb_frame_len = 0; + usb_rx_idx = 0; + } + } + } +} + +/* USB DTR check - monitors DTR signal for disconnect detection + * AND detects 1200bps baud rate for bootloader DFU trigger. */ +static void usb_dtr_check_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + if (!usb_dev) { + return; + } + + uint32_t dtr = 0; + int err = uart_line_ctrl_get(usb_dev, UART_LINE_CTRL_DTR, &dtr); + if (err) { + /* Line control not supported or error */ + k_work_schedule(&usb_dtr_check_work, K_MSEC(USB_DTR_CHECK_MS)); + return; + } + + bool dtr_now = (dtr != 0); + + /* 1200bps DFU trigger: when host opens port at 1200 baud then + * drops DTR, reboot into bootloader DFU mode. + * This is the Arduino convention used by Adafruit nRF52 boards. */ + if (usb_dtr_active && !dtr_now) { + uint32_t baud = 0; + uart_line_ctrl_get(usb_dev, UART_LINE_CTRL_BAUD_RATE, &baud); + if (baud == 1200) { + reboot_to_bootloader_dfu(); + /* Never returns */ + } + + /* Normal DTR drop - USB disconnected */ + LOG_INF("usb_dtr: DTR dropped, USB disconnected"); + if (zephcore_ble_get_active_iface() == ZEPHCORE_IFACE_USB) { + zephcore_ble_set_active_iface(ZEPHCORE_IFACE_NONE); + LOG_INF("usb_dtr: active_iface = IFACE_NONE"); + } + /* Reset RX state */ + ring_buf_reset(&usb_ring_buf); + usb_frame_len = 0; + usb_rx_idx = 0; + } + + usb_dtr_active = dtr_now; + k_work_schedule(&usb_dtr_check_work, K_MSEC(USB_DTR_CHECK_MS)); +} + +/* Send frame over USB with V3 length prefix */ +size_t zephcore_usb_companion_write_frame(const uint8_t *src, size_t len) +{ + if (!usb_dev || len == 0 || len > MAX_FRAME_SIZE) { + return 0; + } + + /* Send length prefix (little-endian) */ + uint8_t len_lo = (uint8_t)(len & 0xFF); + uint8_t len_hi = (uint8_t)((len >> 8) & 0xFF); + + uart_poll_out(usb_dev, len_lo); + uart_poll_out(usb_dev, len_hi); + + /* Send payload */ + for (size_t i = 0; i < len; i++) { + uart_poll_out(usb_dev, src[i]); + } + + LOG_INF("usb_write_frame: sent len=%u hdr=0x%02x", (unsigned)len, src[0]); + return len; +} + +void zephcore_usb_companion_reset_rx(void) +{ + ring_buf_reset(&usb_ring_buf); + usb_frame_len = 0; + usb_rx_idx = 0; +} + +void zephcore_usb_companion_init(struct k_event *mesh_events, + struct k_work *rx_work, + uint32_t mesh_event_ble_rx, + void *board) +{ + s_mesh_events = mesh_events; + s_rx_work = rx_work; + s_mesh_event_ble_rx = mesh_event_ble_rx; + s_board = static_cast(board); + +#if DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) + usb_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart); + if (device_is_ready(usb_dev)) { + LOG_INF("USB CDC device ready: %s", usb_dev->name); + ring_buf_init(&usb_ring_buf, sizeof(usb_ring_buf_data), usb_ring_buf_data); + + /* Set up UART interrupt callback */ + uart_irq_callback_set(usb_dev, usb_uart_isr); + uart_irq_rx_enable(usb_dev); + + /* Start DTR monitoring to detect terminal disconnect */ + k_work_schedule(&usb_dtr_check_work, K_MSEC(USB_DTR_CHECK_MS)); + } else { + LOG_WRN("USB CDC device not ready"); + usb_dev = NULL; + } +#endif +} diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.h b/zephcore/adapters/usb/ZephyrCompanionUSB.h new file mode 100644 index 0000000..4dc1889 --- /dev/null +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.h @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore USB CDC Companion Transport + * + * V3-framed USB CDC for companion mode: ISR, ring buffer, frame parsing, + * DTR monitoring, DFU trigger, and frame write. + */ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize USB CDC companion transport. + * Sets up ring buffer, UART ISR, and DTR polling. + * Call after device_is_ready() for the CDC ACM device. + * + * @param mesh_events Pointer to the k_event used by the mesh event loop + * @param rx_work Pointer to the rx_process work item (for BLE RX event) + * @param mesh_event_ble_rx Bitmask for MESH_EVENT_BLE_RX + * @param board Opaque pointer to ZephyrBoard (for DFU reboot) + */ +void zephcore_usb_companion_init(struct k_event *mesh_events, + struct k_work *rx_work, + uint32_t mesh_event_ble_rx, + void *board); + +/** + * Send a frame over USB with V3 length prefix. + * @return number of payload bytes sent, or 0 on failure + */ +size_t zephcore_usb_companion_write_frame(const uint8_t *src, size_t len); + +/** + * Reset USB RX state (e.g., on BLE disconnect). + */ +void zephcore_usb_companion_reset_rx(void); + +#ifdef __cplusplus +} +#endif diff --git a/zephcore/adapters/usb/ZephyrRepeaterUSB.cpp b/zephcore/adapters/usb/ZephyrRepeaterUSB.cpp new file mode 100644 index 0000000..ec120a2 --- /dev/null +++ b/zephcore/adapters/usb/ZephyrRepeaterUSB.cpp @@ -0,0 +1,178 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore - USB CDC ACM with 1200 baud touch detection (Repeater) + * + * This module initializes USB CDC ACM with a message callback to detect + * 1200 baud touch for entering UF2 bootloader mode. + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_usb_repeater, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +#include + +#include "ZephyrRepeaterUSB.h" + +/* + * USB Device Configuration + * Since we're not using CDC_ACM_SERIAL_INITIALIZE_AT_BOOT, we define our own values. + */ +#define ZEPHCORE_USB_VID 0x2fe3 /* Zephyr Project VID */ +#define ZEPHCORE_USB_PID 0x0004 /* CDC ACM PID */ +#define ZEPHCORE_USB_MAX_POWER 125 /* 250mA in 2mA units */ + +/* + * USB Device Context - based on Zephyr's cdc_acm_serial.c + * but with message callback for 1200 baud touch detection. + */ +USBD_DEVICE_DEFINE(zephcore_usbd, + DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)), + ZEPHCORE_USB_VID, ZEPHCORE_USB_PID); + +USBD_DESC_LANG_DEFINE(zephcore_lang); +USBD_DESC_MANUFACTURER_DEFINE(zephcore_mfr, "ZephCore"); +USBD_DESC_PRODUCT_DEFINE(zephcore_product, "ZephCore Repeater"); +USBD_DESC_SERIAL_NUMBER_DEFINE(zephcore_sn); + +USBD_DESC_CONFIG_DEFINE(zephcore_fs_cfg_desc, "FS Configuration"); +USBD_DESC_CONFIG_DEFINE(zephcore_hs_cfg_desc, "HS Configuration"); + +USBD_CONFIGURATION_DEFINE(zephcore_fs_config, + 0, /* attributes: bus-powered */ + ZEPHCORE_USB_MAX_POWER, &zephcore_fs_cfg_desc); + +USBD_CONFIGURATION_DEFINE(zephcore_hs_config, + 0, /* attributes: bus-powered */ + ZEPHCORE_USB_MAX_POWER, &zephcore_hs_cfg_desc); + +/* ZephyrBoard instance for bootloader entry */ +static mesh::ZephyrBoard usb_board; + +/* + * USB message callback - detect 1200 baud touch for bootloader entry. + * Arduino uses this method: when host opens port at 1200 baud, enter DFU. + */ +static void usbd_msg_callback(struct usbd_context *const ctx, const struct usbd_msg *msg) +{ + if (msg->type == USBD_MSG_CDC_ACM_LINE_CODING) { + uint32_t baudrate = 0; + int ret = uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_BAUD_RATE, &baudrate); + if (ret == 0) { + LOG_DBG("CDC ACM baud rate change: %u", baudrate); + if (baudrate == 1200) { + LOG_INF("1200 baud touch detected - entering bootloader"); + /* Small delay to let USB disconnect cleanly */ + k_msleep(100); + usb_board.rebootToBootloader(); + /* Never returns */ + } + } + } +} + +static int register_cdc_acm(struct usbd_context *const uds_ctx, + const enum usbd_speed speed) +{ + struct usbd_config_node *cfg_nd; + int err; + + if (speed == USBD_SPEED_HS) { + cfg_nd = &zephcore_hs_config; + } else { + cfg_nd = &zephcore_fs_config; + } + + err = usbd_add_configuration(uds_ctx, speed, cfg_nd); + if (err) { + LOG_ERR("Failed to add configuration"); + return err; + } + + err = usbd_register_class(&zephcore_usbd, "cdc_acm_0", speed, 1); + if (err) { + LOG_ERR("Failed to register CDC ACM class"); + return err; + } + + return usbd_device_set_code_triple(uds_ctx, speed, + USB_BCC_MISCELLANEOUS, 0x02, 0x01); +} + +extern "C" int zephcore_usbd_init(void) +{ + int err; + + LOG_INF("Initializing ZephCore USB CDC with 1200 baud touch detection"); + + /* Add string descriptors */ + err = usbd_add_descriptor(&zephcore_usbd, &zephcore_lang); + if (err) { + LOG_ERR("Failed to add language descriptor (%d)", err); + return err; + } + + err = usbd_add_descriptor(&zephcore_usbd, &zephcore_mfr); + if (err) { + LOG_ERR("Failed to add manufacturer descriptor (%d)", err); + return err; + } + + err = usbd_add_descriptor(&zephcore_usbd, &zephcore_product); + if (err) { + LOG_ERR("Failed to add product descriptor (%d)", err); + return err; + } + + err = usbd_add_descriptor(&zephcore_usbd, &zephcore_sn); + if (err) { + LOG_ERR("Failed to add serial number descriptor (%d)", err); + return err; + } + + /* Register configurations */ + if (USBD_SUPPORTS_HIGH_SPEED && + usbd_caps_speed(&zephcore_usbd) == USBD_SPEED_HS) { + err = register_cdc_acm(&zephcore_usbd, USBD_SPEED_HS); + if (err) { + return err; + } + } + + err = register_cdc_acm(&zephcore_usbd, USBD_SPEED_FS); + if (err) { + return err; + } + + /* Register message callback for 1200 baud detection */ + err = usbd_msg_register_cb(&zephcore_usbd, usbd_msg_callback); + if (err) { + LOG_ERR("Failed to register message callback (%d)", err); + return err; + } + + /* Initialize USB device */ + err = usbd_init(&zephcore_usbd); + if (err) { + LOG_ERR("Failed to initialize USB device (%d)", err); + return err; + } + + /* Enable USB device */ + err = usbd_enable(&zephcore_usbd); + if (err) { + LOG_ERR("Failed to enable USB device (%d)", err); + return err; + } + + LOG_INF("ZephCore USB CDC initialized"); + return 0; +} diff --git a/zephcore/adapters/usb/ZephyrRepeaterUSB.h b/zephcore/adapters/usb/ZephyrRepeaterUSB.h new file mode 100644 index 0000000..4634acf --- /dev/null +++ b/zephcore/adapters/usb/ZephyrRepeaterUSB.h @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore - USB CDC ACM with 1200 baud touch detection (Repeater) + * + * Replaces Zephyr's CDC_ACM_SERIAL_INITIALIZE_AT_BOOT with custom init + * that includes a message callback for detecting 1200 baud touch to + * enter UF2 bootloader mode. + */ + +#ifndef ZEPHCORE_ZEPHYR_REPEATER_USB_H +#define ZEPHCORE_ZEPHYR_REPEATER_USB_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize USB CDC ACM with 1200 baud touch detection. + * + * This replaces Zephyr's default CDC_ACM_SERIAL_INITIALIZE_AT_BOOT + * initialization with our own that includes a message callback for + * detecting 1200 baud touch to enter bootloader mode. + * + * @return 0 on success, negative error code on failure. + */ +int zephcore_usbd_init(void); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHCORE_ZEPHYR_REPEATER_USB_H */ diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp new file mode 100644 index 0000000..59305e9 --- /dev/null +++ b/zephcore/app/CompanionMesh.cpp @@ -0,0 +1,2573 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * CompanionMesh implementation + */ + +#include "CompanionMesh.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +LOG_MODULE_REGISTER(zephcore_companion, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); + +/* Protocol commands (matches Arduino companion_radio) - sorted by opcode */ +#define CMD_APP_START 0x01 +#define CMD_SEND_TXT_MSG 0x02 +#define CMD_SEND_CHANNEL_TXT_MSG 0x03 +#define CMD_GET_CONTACTS 0x04 +#define CMD_GET_DEVICE_TIME 0x05 +#define CMD_SET_DEVICE_TIME 0x06 +#define CMD_SEND_SELF_ADVERT 0x07 +#define CMD_SET_ADVERT_NAME 0x08 +#define CMD_ADD_UPDATE_CONTACT 0x09 +#define CMD_SYNC_NEXT_MESSAGE 0x0A +#define CMD_SET_RADIO_PARAMS 0x0B +#define CMD_SET_RADIO_TX_POWER 0x0C +#define CMD_RESET_PATH 0x0D +#define CMD_SET_ADVERT_LATLON 0x0E +#define CMD_REMOVE_CONTACT 0x0F +#define CMD_SHARE_CONTACT 0x10 +#define CMD_EXPORT_CONTACT 0x11 +#define CMD_IMPORT_CONTACT 0x12 +#define CMD_REBOOT 0x13 +#define CMD_GET_BATT_AND_STORAGE 0x14 +#define CMD_SET_TUNING_PARAMS 0x15 +#define CMD_DEVICE_QUERY 0x16 +#define CMD_EXPORT_PRIVATE_KEY 0x17 +#define CMD_IMPORT_PRIVATE_KEY 0x18 +#define CMD_SEND_RAW_DATA 0x19 +#define CMD_SEND_LOGIN 0x1A +#define CMD_SEND_STATUS_REQ 0x1B +#define CMD_HAS_CONNECTION 0x1C +#define CMD_LOGOUT 0x1D +#define CMD_GET_CONTACT_BY_KEY 0x1E +#define CMD_GET_CHANNEL 0x1F +#define CMD_SET_CHANNEL 0x20 +#define CMD_SIGN_START 0x21 +#define CMD_SIGN_DATA 0x22 +#define CMD_SIGN_FINISH 0x23 +#define CMD_SEND_TRACE_PATH 0x24 +#define CMD_SET_DEVICE_PIN 0x25 +#define CMD_SET_OTHER_PARAMS 0x26 +#define CMD_SEND_TELEMETRY_REQ 0x27 +#define CMD_GET_CUSTOM_VARS 0x28 +#define CMD_SET_CUSTOM_VAR 0x29 +#define CMD_GET_ADVERT_PATH 0x2A +#define CMD_GET_TUNING_PARAMS 0x2B +#define CMD_SEND_BINARY_REQ 0x32 +#define CMD_FACTORY_RESET 0x33 +#define CMD_SEND_PATH_DISCOVERY 0x34 +#define CMD_SET_FLOOD_SCOPE 0x36 +#define CMD_SEND_CONTROL_DATA 0x37 +#define CMD_GET_STATS 0x38 +#define CMD_SEND_ANON_REQ 0x39 +#define CMD_SET_AUTOADD_CONFIG 0x3A +#define CMD_GET_AUTOADD_CONFIG 0x3B +#define CMD_GET_ALLOWED_REPEAT_FREQ 0x3C + +/* Response packet types */ +#define PACKET_OK 0x00 +#define PACKET_ERROR 0x01 +#define PACKET_CONTACT_START 0x02 +#define PACKET_CONTACT 0x03 +#define PACKET_CONTACT_END 0x04 +#define PACKET_SELF_INFO 0x05 +#define PACKET_SENT 0x06 +#define PACKET_CONTACT_MSG_RECV 0x07 /* Legacy - ver < 3 */ +#define PACKET_CHANNEL_MSG_RECV 0x08 /* Legacy - ver < 3 */ +#define PACKET_CURR_TIME 0x09 +#define PACKET_NO_MORE_MSGS 0x0A +#define PACKET_EXPORT_CONTACT 0x0B +#define PACKET_BATTERY 0x0C +#define PACKET_DEVICE_INFO 0x0D +#define PACKET_PRIVATE_KEY 0x0E +#define PACKET_DISABLED 0x0F +#define PACKET_CONTACT_MSG_V3 0x10 /* Contact message for app ver >= 3 */ +#define PACKET_CHANNEL_MSG_V3 0x11 /* Channel message for app ver >= 3 */ +#define PACKET_CHANNEL_INFO 0x12 +#define PACKET_SIGN_START 0x13 +#define PACKET_SIGNATURE 0x14 +#define PACKET_CUSTOM_VARS 0x15 +#define PACKET_ADVERT_PATH 0x16 +#define PACKET_TUNING_PARAMS 0x17 +#define PACKET_STATS 0x18 +#define PACKET_AUTOADD_CONFIG 0x19 +#define PACKET_ALLOWED_REPEAT_FREQ 0x1A + +#define MAX_SIGN_DATA_LEN (8 * 1024) + +/* Error codes */ +#define ERR_UNSUPPORTED 0x01 +#define ERR_NOT_FOUND 0x02 +#define ERR_TABLE_FULL 0x03 +#define ERR_BAD_STATE 0x04 +#define ERR_ILLEGAL_ARG 0x06 + +/* Max LoRa TX power (dBm) for SX1262 */ +#define MAX_LORA_TX_POWER 22 + +/* Helper to put little-endian values */ +static inline void put_le16(uint8_t *p, uint16_t v) { p[0] = v & 0xFF; p[1] = (v >> 8) & 0xFF; } +static inline void put_le32(uint8_t *p, uint32_t v) { p[0] = v & 0xFF; p[1] = (v >> 8) & 0xFF; p[2] = (v >> 16) & 0xFF; p[3] = (v >> 24) & 0xFF; } +static inline uint32_t get_le32(const uint8_t *p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); } + +/* Allowed client repeat frequency ranges (matches Arduino) */ +struct FreqRange { + uint32_t lower_freq, upper_freq; +}; + +static const FreqRange repeat_freq_ranges[] = { + { 433000, 433000 }, /* 433 MHz (ISM band) */ + { 869000, 869000 }, /* 869 MHz (EU ISM band) */ + { 918000, 918000 }, /* 918 MHz (US ISM band) */ +}; + +static bool isValidClientRepeatFreq(uint32_t f) +{ + for (size_t i = 0; i < sizeof(repeat_freq_ranges) / sizeof(repeat_freq_ranges[0]); i++) { + if (f >= repeat_freq_ranges[i].lower_freq && f <= repeat_freq_ranges[i].upper_freq) { + return true; + } + } + return false; +} + +CompanionMesh::CompanionMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng, + mesh::RTCClock &rtc, mesh::PacketManager &mgr, mesh::MeshTables &tables, + ZephyrDataStore &store) + : BaseChatMesh(radio, ms, rng, rtc, mgr, tables), _store(&store) +{ + _push_cb = nullptr; + _write_cb = nullptr; + _batt_cb = nullptr; + _radio_reconfig_cb = nullptr; + _pin_change_cb = nullptr; + _contact_iter_active = false; + _contact_iter_idx = 0; + _contact_iter_lastmod = 0; + _contact_iter_since = 0; + _offline_queue_head = 0; + _offline_queue_tail = 0; + _offline_queue_count = 0; + memset(_ack_table, 0, sizeof(_ack_table)); + _ack_next_overwrite = 0; + memset(_advert_paths, 0, sizeof(_advert_paths)); + _next_advert_path_idx = 0; + _sign_data = nullptr; + _sign_data_len = 0; + _sign_data_capacity = 0; + _pending_login = 0; + _pending_status = 0; + _pending_telemetry = 0; + _pending_discovery = 0; + _pending_req = 0; + _app_target_ver = 0; + _dirty_contacts_expiry = 0; + _dirty_channels_expiry = 0; + memset(_send_scope.key, 0, sizeof(_send_scope.key)); + memset(&prefs, 0, sizeof(prefs)); + prefs.node_lat = 0; + prefs.node_lon = 0; +} + +void CompanionMesh::begin() +{ + BaseChatMesh::begin(); +} + +bool CompanionMesh::allowPacketForward(const mesh::Packet *packet) +{ + (void)packet; + return prefs.client_repeat != 0; +} + +void CompanionMesh::sendFloodScoped(const ContactInfo &recipient, mesh::Packet *pkt, uint32_t delay_millis) +{ + if (_send_scope.isNull()) { + sendFlood(pkt, delay_millis); + } else { + uint16_t codes[2]; + codes[0] = _send_scope.calcTransportCode(pkt); + codes[1] = 0; + sendFlood(pkt, codes, delay_millis); + } +} + +void CompanionMesh::sendFloodScoped(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t delay_millis) +{ + if (_send_scope.isNull()) { + sendFlood(pkt, delay_millis); + } else { + uint16_t codes[2]; + codes[0] = _send_scope.calcTransportCode(pkt); + codes[1] = 0; + sendFlood(pkt, codes, delay_millis); + } +} + +bool CompanionMesh::onContactPathRecv(ContactInfo &from, uint8_t *in_path, uint8_t in_path_len, + uint8_t *out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) +{ + if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) { + uint32_t tag; + memcpy(&tag, extra, 4); + + if (tag == _pending_discovery) { + _pending_discovery = 0; + + if (in_path_len <= MAX_PATH_SIZE && out_path_len <= MAX_PATH_SIZE) { + uint8_t rsp[172]; + int i = 0; + rsp[i++] = PUSH_CODE_PATH_DISCOVERY_RESP; + rsp[i++] = 0; // reserved + memcpy(&rsp[i], from.id.pub_key, 6); + i += 6; + rsp[i++] = out_path_len; + memcpy(&rsp[i], out_path, out_path_len); + i += out_path_len; + rsp[i++] = in_path_len; + memcpy(&rsp[i], in_path, in_path_len); + i += in_path_len; + sendPush(rsp[0], &rsp[1], i - 1); + } else { + LOG_WRN("onContactPathRecv: invalid path sizes: out=%d in=%d", + out_path_len, in_path_len); + } + return false; // don't send reciprocal path + } + } + // let base class handle non-discovery path responses + return BaseChatMesh::onContactPathRecv(from, in_path, in_path_len, + out_path, out_path_len, extra_type, extra, extra_len); +} + +void CompanionMesh::loop() +{ + BaseChatMesh::loop(); + + /* Check for pending lazy contact/channel writes */ + int64_t now = _ms->getMillis(); + if (_dirty_contacts_expiry && now >= _dirty_contacts_expiry) { + flushDirtyContacts(); + } + if (_dirty_channels_expiry && now >= _dirty_channels_expiry) { + flushDirtyChannels(); + } +} + +void CompanionMesh::markContactsDirty() +{ + _dirty_contacts_expiry = _ms->getMillis() + LAZY_WRITE_DELAY_MS; +} + +void CompanionMesh::markChannelsDirty() +{ + _dirty_channels_expiry = _ms->getMillis() + LAZY_WRITE_DELAY_MS; +} + +void CompanionMesh::flushDirtyContacts() +{ + if (_dirty_contacts_expiry) { + LOG_INF("flushDirtyContacts: saving contacts (lazy write)"); + _store->saveContacts(this); + _dirty_contacts_expiry = 0; + } +} + +void CompanionMesh::flushDirtyChannels() +{ + if (_dirty_channels_expiry) { + LOG_INF("flushDirtyChannels: saving channels (lazy write)"); + _store->saveChannels(this); + _dirty_channels_expiry = 0; + } +} + +void CompanionMesh::writeFrame(const uint8_t *data, size_t len) +{ + LOG_DBG("RSP: 0x%02x len=%u", data[0], (unsigned)len); + if (_write_cb) { + _write_cb(data, len); + } +} + +void CompanionMesh::sendPacketOk() +{ + uint8_t rsp[] = { PACKET_OK }; + writeFrame(rsp, sizeof(rsp)); +} + +void CompanionMesh::sendPacketError(uint8_t code) +{ + uint8_t rsp[] = { PACKET_ERROR, code }; + writeFrame(rsp, sizeof(rsp)); +} + +void CompanionMesh::sendPush(uint8_t code, const uint8_t *data, size_t len) +{ + LOG_INF("sendPush: code=0x%02x len=%u _push_cb=%s", code, (unsigned)len, _push_cb ? "set" : "NULL"); + if (_push_cb) { + _push_cb(code, data, len); + } else { + LOG_WRN("sendPush: _push_cb is NULL, push lost!"); + } +} + +size_t CompanionMesh::serializeContact(uint8_t *buf, const ContactInfo &c, uint8_t header) +{ + size_t i = 0; + if (header) { + buf[i++] = header; + } + memcpy(&buf[i], c.id.pub_key, PUB_KEY_SIZE); i += PUB_KEY_SIZE; + buf[i++] = c.type; + buf[i++] = c.flags; + buf[i++] = (c.out_path_len < 0) ? 0xFF : (uint8_t)c.out_path_len; + memcpy(&buf[i], c.out_path, MAX_PATH_SIZE); i += MAX_PATH_SIZE; + StrHelper::strzcpy((char *)&buf[i], c.name, 32); i += 32; + put_le32(&buf[i], c.last_advert_timestamp); i += 4; + put_le32(&buf[i], (uint32_t)c.gps_lat); i += 4; + put_le32(&buf[i], (uint32_t)c.gps_lon); i += 4; + put_le32(&buf[i], c.lastmod); i += 4; + return i; +} + +static bool isChannelMessage(const uint8_t *buf) +{ + // Channel messages have PACKET_CHANNEL_MSG_V3 (0x11) or PACKET_CHANNEL_MSG_RECV (0x08) + return buf[0] == PACKET_CHANNEL_MSG_V3 || buf[0] == PACKET_CHANNEL_MSG_RECV; +} + +void CompanionMesh::queueOfflineMessage(const uint8_t *data, size_t len) +{ + LOG_INF("queueOfflineMessage: len=%u type=0x%02x count_before=%d", (unsigned)len, data[0], _offline_queue_count); + if (_offline_queue_count >= OFFLINE_QUEUE_SIZE) { + // Queue full - try to drop oldest channel message first + int pos = _offline_queue_head; + for (int i = 0; i < _offline_queue_count; i++) { + int idx = (pos + i) % OFFLINE_QUEUE_SIZE; + if (isChannelMessage(_offline_queue[idx].buf)) { + // Found a channel message, remove it + for (int j = i; j < _offline_queue_count - 1; j++) { + int from = (_offline_queue_head + j + 1) % OFFLINE_QUEUE_SIZE; + int to = (_offline_queue_head + j) % OFFLINE_QUEUE_SIZE; + _offline_queue[to] = _offline_queue[from]; + } + _offline_queue_tail = (_offline_queue_tail - 1 + OFFLINE_QUEUE_SIZE) % OFFLINE_QUEUE_SIZE; + _offline_queue_count--; + break; + } + } + // If still full (no channel messages found), drop oldest + if (_offline_queue_count >= OFFLINE_QUEUE_SIZE) { + _offline_queue_head = (_offline_queue_head + 1) % OFFLINE_QUEUE_SIZE; + _offline_queue_count--; + } + } + QueuedFrame *f = &_offline_queue[_offline_queue_tail]; + f->len = (len > sizeof(f->buf)) ? sizeof(f->buf) : len; + memcpy(f->buf, data, f->len); + _offline_queue_tail = (_offline_queue_tail + 1) % OFFLINE_QUEUE_SIZE; + _offline_queue_count++; + LOG_INF("queueOfflineMessage: count_after=%d", _offline_queue_count); +} + +bool CompanionMesh::dequeueOfflineMessage(uint8_t *dest, size_t &len) +{ + if (_offline_queue_count == 0) return false; + QueuedFrame *f = &_offline_queue[_offline_queue_head]; + len = f->len; + memcpy(dest, f->buf, len); + _offline_queue_head = (_offline_queue_head + 1) % OFFLINE_QUEUE_SIZE; + _offline_queue_count--; + return true; +} + +void CompanionMesh::resetContactIterator() +{ + if (_contact_iter_active) { + // Send contact end if interrupted + uint8_t rsp[5]; + rsp[0] = PACKET_CONTACT_END; + put_le32(&rsp[1], _contact_iter_lastmod); + writeFrame(rsp, sizeof(rsp)); + } + _contact_iter_active = false; +} + +bool CompanionMesh::continueContactIteration() +{ + if (!_contact_iter_active) return false; + + if (_contact_iter_idx < getNumContacts()) { + ContactInfo c; + if (getContactByIdx(_contact_iter_idx, c)) { + // Apply 'since' filter - only send contacts modified after the timestamp + if (c.lastmod > _contact_iter_since) { + if (c.lastmod > _contact_iter_lastmod) { + _contact_iter_lastmod = c.lastmod; + } + uint8_t rsp[CONTACT_FRAME_SIZE]; + size_t n = serializeContact(rsp, c, PACKET_CONTACT); + writeFrame(rsp, n); + } + } + _contact_iter_idx++; + return true; + } else { + // Send PACKET_CONTACT_END with most_recent_lastmod + uint8_t rsp[5]; + rsp[0] = PACKET_CONTACT_END; + put_le32(&rsp[1], _contact_iter_lastmod); + writeFrame(rsp, sizeof(rsp)); + _contact_iter_active = false; + return false; + } +} + +void CompanionMesh::addPendingAck(uint32_t expected, int contact_idx) +{ + uint32_t now_ms = (uint32_t)_ms->getMillis(); + for (int i = 0; i < ACK_TABLE_SIZE; i++) { + if (!_ack_table[i].active) { + _ack_table[i].expected_ack = expected; + _ack_table[i].contact_idx = contact_idx; + _ack_table[i].sent_time = now_ms; + _ack_table[i].active = true; + return; + } + } + // Table full — circular overwrite + int idx = _ack_next_overwrite; + _ack_table[idx].expected_ack = expected; + _ack_table[idx].contact_idx = contact_idx; + _ack_table[idx].sent_time = now_ms; + _ack_table[idx].active = true; + _ack_next_overwrite = (idx + 1) % ACK_TABLE_SIZE; +} + +int CompanionMesh::findAndRemoveAck(uint32_t ack, uint32_t *out_sent_time) +{ + for (int i = 0; i < ACK_TABLE_SIZE; i++) { + if (_ack_table[i].active && _ack_table[i].expected_ack == ack) { + if (out_sent_time) { + *out_sent_time = _ack_table[i].sent_time; + } + _ack_table[i].active = false; + return _ack_table[i].contact_idx; + } + } + return -1; +} + +/* DataStoreHost interface */ +bool CompanionMesh::onContactLoaded(const ContactInfo &c) +{ + return addContact(c); +} + +bool CompanionMesh::getContactForSave(uint32_t idx, ContactInfo &c) +{ + return getContactByIdx(idx, c); +} + +bool CompanionMesh::onChannelLoaded(uint8_t idx, const ChannelDetails &ch) +{ + return setChannel(idx, ch); +} + +bool CompanionMesh::getChannelForSave(uint8_t idx, ChannelDetails &ch) +{ + // Only save channels that have content (non-empty name or non-zero secret) + if (getChannel(idx, ch)) { + // Check if channel has any content + if (ch.name[0] != '\0') return true; + // Check if secret is non-zero + static const uint8_t zeroes[32] = {0}; + if (memcmp(ch.channel.secret, zeroes, 32) != 0) return true; + } + return false; +} + +/* Storage interface */ +int CompanionMesh::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) +{ + return _store->getBlobByKey(key, key_len, dest_buf); +} + +bool CompanionMesh::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) +{ + return _store->putBlobByKey(key, key_len, src_buf, len); +} + +/* BaseChatMesh virtual implementations */ +void CompanionMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t *path) +{ + LOG_INF("onDiscoveredContact: '%s' is_new=%d path_len=%d num_contacts=%d", + contact.name, is_new, path_len, getNumContacts()); + + // Mark contacts dirty for lazy save + markContactsDirty(); + + // Update advert path table + AdvertPath *ap = &_advert_paths[_next_advert_path_idx]; + memcpy(ap->pubkey_prefix, contact.id.pub_key, 7); + ap->path_len = path_len; + if (path_len > 0 && path) { + memcpy(ap->path, path, (path_len < MAX_PATH_SIZE) ? path_len : MAX_PATH_SIZE); + } + memcpy(ap->name, contact.name, sizeof(ap->name)); + ap->recv_timestamp = (uint32_t)getRTCClock()->getCurrentTime(); + _next_advert_path_idx = (_next_advert_path_idx + 1) % ADVERT_PATH_TABLE_SIZE; + + // Send push notification + if (is_new) { + uint8_t rsp[CONTACT_FRAME_SIZE]; + size_t n = serializeContact(rsp, contact); /* no header — push code is separate */ + LOG_INF("onDiscoveredContact: sending PUSH_CODE_NEW_ADVERT (full contact)"); + sendPush(PUSH_CODE_NEW_ADVERT, rsp, n); + } else { + // ADVERT: send just pubkey + LOG_INF("onDiscoveredContact: sending PUSH_CODE_ADVERT (pubkey only)"); + sendPush(PUSH_CODE_ADVERT, contact.id.pub_key, PUB_KEY_SIZE); + } +} + +ContactInfo *CompanionMesh::processAck(const uint8_t *data) +{ + uint32_t ack_crc; + memcpy(&ack_crc, data, 4); + LOG_INF("processAck: received ack_crc=0x%08x", ack_crc); + + uint32_t sent_time = 0; + int contact_idx = findAndRemoveAck(ack_crc, &sent_time); + LOG_INF("processAck: findAndRemoveAck returned idx=%d sent_time=%u", contact_idx, sent_time); + if (contact_idx >= 0) { + ContactInfo ci; + if (getContactByIdx(contact_idx, ci)) { + LOG_INF("processAck: ACK matched contact '%s', sending push", ci.name); + + // Send push notification in Arduino-compatible format: + // [PUSH_CODE_SEND_CONFIRMED] + [4-byte ack_crc] + [4-byte trip_time] + uint8_t ack_push[8]; // 4 bytes ack + 4 bytes trip_time + memcpy(ack_push, data, 4); // ack_crc + uint32_t now = (uint32_t)_ms->getMillis(); + uint32_t trip_time = now - sent_time; + put_le32(&ack_push[4], trip_time); + LOG_INF("processAck: ack_push ack_crc=0x%08x trip_time=%u", ack_crc, trip_time); + sendPush(PUSH_CODE_SEND_CONFIRMED, ack_push, 8); + + return lookupContactByPubKey(ci.id.pub_key, PUB_KEY_SIZE); + } + } + LOG_INF("processAck: no matching pending ACK, checking connections"); + return checkConnectionsAck(data); +} + +void CompanionMesh::onContactPathUpdated(const ContactInfo &contact) +{ + markContactsDirty(); + sendPush(PUSH_CODE_PATH_UPDATED, contact.id.pub_key, PUB_KEY_SIZE); +} + +void CompanionMesh::onMessageRecv(const ContactInfo &contact, mesh::Packet *pkt, + uint32_t sender_timestamp, const char *text) +{ + LOG_INF("onMessageRecv: from '%s' timestamp=%u text='%s'", + contact.name, sender_timestamp, text); + + markConnectionActive(contact); + queueContactMessage(contact, pkt, TXT_TYPE_PLAIN, sender_timestamp, nullptr, 0, text); + sendPush(PUSH_CODE_MSG_WAITING); +} + +void CompanionMesh::queueContactMessage(const ContactInfo &contact, mesh::Packet *pkt, + uint8_t txt_type, uint32_t sender_timestamp, const uint8_t *extra, int extra_len, const char *text) +{ + // Build incoming message frame for offline queue + uint8_t frame[MAX_FRAME_SIZE]; + int i = 0; + + if (_app_target_ver >= 3) { + // V3 format: [0x10][SNR*4][0][0][6-byte pubkey prefix][path_len|0xFF][txt_type][4-byte timestamp][extra][text] + frame[i++] = PACKET_CONTACT_MSG_V3; + frame[i++] = pkt ? (int8_t)(pkt->getSNR() * 4) : 0; // SNR * 4 + frame[i++] = 0; // reserved1 + frame[i++] = 0; // reserved2 + } else { + // V2 format: [0x07][6-byte pubkey prefix][path_len|0xFF][txt_type][4-byte timestamp][extra][text] + frame[i++] = PACKET_CONTACT_MSG_RECV; + } + + // 6-byte pubkey prefix + memcpy(&frame[i], contact.id.pub_key, 6); + i += 6; + + // path_len: 0xFF if direct, else actual path_len + frame[i++] = (pkt && pkt->isRouteFlood()) ? pkt->path_len : 0xFF; + + // txt_type + frame[i++] = txt_type; + + // 4-byte timestamp (little-endian) + put_le32(&frame[i], sender_timestamp); + i += 4; + + // extra data (e.g., for signed messages) + if (extra_len > 0 && extra) { + memcpy(&frame[i], extra, extra_len); + i += extra_len; + } + + // text + size_t text_len = strlen(text); + if (i + text_len > sizeof(frame)) { + text_len = sizeof(frame) - i; + } + memcpy(&frame[i], text, text_len); + i += text_len; + + LOG_INF("queueContactMessage: frame_len=%d type=0x%02x", i, frame[0]); + queueOfflineMessage(frame, i); +} + +void CompanionMesh::onCommandDataRecv(const ContactInfo &contact, mesh::Packet *pkt, + uint32_t sender_timestamp, const char *text) +{ + LOG_INF("onCommandDataRecv: from '%s' timestamp=%u text='%s'", + contact.name, sender_timestamp, text); + + markConnectionActive(contact); + queueContactMessage(contact, pkt, TXT_TYPE_CLI_DATA, sender_timestamp, nullptr, 0, text); + sendPush(PUSH_CODE_MSG_WAITING); +} + +void CompanionMesh::onSignedMessageRecv(const ContactInfo &contact, mesh::Packet *pkt, + uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) +{ + LOG_INF("onSignedMessageRecv: from '%s' timestamp=%u text='%s'", + contact.name, sender_timestamp, text); + + markConnectionActive(contact); + markContactsDirty(); + // sender_prefix is 4 bytes + queueContactMessage(contact, pkt, TXT_TYPE_SIGNED_PLAIN, sender_timestamp, sender_prefix, 4, text); + sendPush(PUSH_CODE_MSG_WAITING); +} + +uint32_t CompanionMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const +{ + return 500 + (uint32_t)(16.0f * pkt_airtime_millis); +} + +uint32_t CompanionMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const +{ + return 500 + (uint32_t)((pkt_airtime_millis * 6.0f + 250) * (path_len + 1)); +} + +void CompanionMesh::onSendTimeout() +{ + // Message send timed out - could notify UI +} + +void CompanionMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, + uint32_t timestamp, const char *text) +{ + LOG_INF("onChannelMessageRecv: timestamp=%u text='%s'", timestamp, text); + + // Build incoming channel message frame + uint8_t frame[MAX_FRAME_SIZE]; + int i = 0; + + if (_app_target_ver >= 3) { + // V3: [0x11][SNR*4][0][0][channel_idx][path_len|0xFF][txt_type][4-byte timestamp][text] + frame[i++] = PACKET_CHANNEL_MSG_V3; + frame[i++] = pkt ? (int8_t)(pkt->getSNR() * 4) : 0; + frame[i++] = 0; // reserved1 + frame[i++] = 0; // reserved2 + } else { + // V2: [0x08][channel_idx][path_len|0xFF][txt_type][4-byte timestamp][text] + frame[i++] = PACKET_CHANNEL_MSG_RECV; + } + + // channel index + uint8_t channel_idx = findChannelIdx(channel); + frame[i++] = channel_idx; + + // path_len: 0xFF if direct, else actual path_len + frame[i++] = (pkt && pkt->isRouteFlood()) ? pkt->path_len : 0xFF; + + // txt_type + frame[i++] = TXT_TYPE_PLAIN; + + // 4-byte timestamp (little-endian) + put_le32(&frame[i], timestamp); + i += 4; + + // text + size_t text_len = strlen(text); + if (i + text_len > sizeof(frame)) { + text_len = sizeof(frame) - i; + } + memcpy(&frame[i], text, text_len); + i += text_len; + + LOG_INF("onChannelMessageRecv: frame_len=%d channel_idx=%d", i, channel_idx); + queueOfflineMessage(frame, i); + sendPush(PUSH_CODE_MSG_WAITING); +} + +uint8_t CompanionMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, + const uint8_t *data, uint8_t len, uint8_t *reply) +{ + if (len < 1) return 0; + + if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) { + // Calculate permissions based on prefs and contact flags + uint8_t permissions = 0; + uint8_t cp = contact.flags >> 1; // LSB is 'favourite' bit, use upper bits for perms + + if (prefs.telemetry_mode_base == TELEM_MODE_ALLOW_ALL) { + permissions = TELEM_PERM_BASE; + } else if (prefs.telemetry_mode_base == TELEM_MODE_ALLOW_FLAGS) { + permissions = cp & TELEM_PERM_BASE; + } + + if (prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_ALL) { + permissions |= TELEM_PERM_LOCATION; + } else if (prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_FLAGS) { + permissions |= cp & TELEM_PERM_LOCATION; + } + + if (prefs.telemetry_mode_env == TELEM_MODE_ALLOW_ALL) { + permissions |= TELEM_PERM_ENVIRONMENT; + } else if (prefs.telemetry_mode_env == TELEM_MODE_ALLOW_FLAGS) { + permissions |= cp & TELEM_PERM_ENVIRONMENT; + } + + // Apply permission mask from request (if present) + if (len > 1) { + uint8_t perm_mask = ~data[1]; // First reserved byte is inverse mask + permissions &= perm_mask; + } + + // Only respond if base permission is granted + if (permissions & TELEM_PERM_BASE) { + LOG_INF("onContactRequest: telemetry authorized (perms=0x%02x)", permissions); + + // Build Cayenne LPP telemetry response + int i = 0; + + // Reflect sender_timestamp back as tag (4 bytes) + memcpy(reply, &sender_timestamp, 4); + i += 4; + + const uint8_t CH_SELF = 1; + + // Battery voltage: [channel][LPP_VOLTAGE=116][2-byte 0.01V big-endian] + uint16_t batt_mv = _batt_cb ? _batt_cb() : 0; + reply[i++] = CH_SELF; + reply[i++] = 116; // LPP_VOLTAGE + uint16_t batt_scaled = batt_mv / 10; + reply[i++] = (batt_scaled >> 8) & 0xFF; + reply[i++] = batt_scaled & 0xFF; + + // GPS position if authorized and available + if (permissions & TELEM_PERM_LOCATION) { + struct gps_position pos; + if (gps_is_available() && gps_get_last_known_position(&pos)) { + reply[i++] = CH_SELF; + reply[i++] = 136; // LPP_GPS + int32_t lat = (int32_t)(pos.latitude_ndeg / 100000); + int32_t lon = (int32_t)(pos.longitude_ndeg / 100000); + int32_t alt = pos.altitude_mm / 10; + reply[i++] = (lat >> 16) & 0xFF; + reply[i++] = (lat >> 8) & 0xFF; + reply[i++] = lat & 0xFF; + reply[i++] = (lon >> 16) & 0xFF; + reply[i++] = (lon >> 8) & 0xFF; + reply[i++] = lon & 0xFF; + reply[i++] = (alt >> 16) & 0xFF; + reply[i++] = (alt >> 8) & 0xFF; + reply[i++] = alt & 0xFF; + } else if (prefs.node_lat != 0 || prefs.node_lon != 0) { + // Use configured position + reply[i++] = CH_SELF; + reply[i++] = 136; // LPP_GPS + int32_t lat = (int32_t)(prefs.node_lat * 10000); + int32_t lon = (int32_t)(prefs.node_lon * 10000); + int32_t alt = 0; + reply[i++] = (lat >> 16) & 0xFF; + reply[i++] = (lat >> 8) & 0xFF; + reply[i++] = lat & 0xFF; + reply[i++] = (lon >> 16) & 0xFF; + reply[i++] = (lon >> 8) & 0xFF; + reply[i++] = lon & 0xFF; + reply[i++] = (alt >> 16) & 0xFF; + reply[i++] = (alt >> 8) & 0xFF; + reply[i++] = alt & 0xFF; + } + } + + // Environment sensors if authorized and available + if (permissions & TELEM_PERM_ENVIRONMENT) { + struct env_data env; + if (env_sensors_read(&env) == 0) { + if (env.has_temperature) { + reply[i++] = CH_SELF; + reply[i++] = LPP_TEMPERATURE; + int16_t temp = (int16_t)(env.temperature_c * 10); + reply[i++] = (temp >> 8) & 0xFF; + reply[i++] = temp & 0xFF; + } else if (env.has_mcu_temperature) { + // MCU die temp as fallback when no external sensor + reply[i++] = CH_SELF; + reply[i++] = LPP_TEMPERATURE; + int16_t temp = (int16_t)(env.mcu_temperature_c * 10); + reply[i++] = (temp >> 8) & 0xFF; + reply[i++] = temp & 0xFF; + } + if (env.has_humidity) { + reply[i++] = CH_SELF; + reply[i++] = LPP_RELATIVE_HUMIDITY; + reply[i++] = (uint8_t)(env.humidity_pct * 2); + } + if (env.has_pressure) { + reply[i++] = CH_SELF; + reply[i++] = LPP_BAROMETRIC_PRESSURE; + uint16_t press = (uint16_t)(env.pressure_hpa * 10); + reply[i++] = (press >> 8) & 0xFF; + reply[i++] = press & 0xFF; + } + } + + // Power monitor telemetry (INA219/INA3221/ina2xx) + if (power_sensors_available()) { + struct power_data pwr; + if (power_sensors_read(&pwr) == 0) { + uint8_t ch = CH_SELF + 1; + for (int j = 0; j < pwr.num_channels; j++) { + if (pwr.channels[j].valid) { + // Voltage: [ch][LPP_VOLTAGE=116][2-byte 0.01V] + reply[i++] = ch; + reply[i++] = 116; + uint16_t v = (uint16_t)(pwr.channels[j].voltage_v * 100); + reply[i++] = (v >> 8) & 0xFF; + reply[i++] = v & 0xFF; + // Current: [ch][LPP_CURRENT=117][2-byte 0.001A] + reply[i++] = ch; + reply[i++] = 117; + uint16_t c = (uint16_t)(pwr.channels[j].current_a * 1000); + reply[i++] = (c >> 8) & 0xFF; + reply[i++] = c & 0xFF; + // Power: [ch][LPP_POWER=128][2-byte 1W] + reply[i++] = ch; + reply[i++] = 128; + uint16_t p = (uint16_t)(pwr.channels[j].power_w); + reply[i++] = (p >> 8) & 0xFF; + reply[i++] = p & 0xFF; + ch++; + } + } + } + } + } + + // Trigger GPS wake for fresh fix on next request + if (gps_is_available() && gps_is_enabled()) { + gps_request_fresh_fix(); + } + + return i; + } else { + LOG_INF("onContactRequest: telemetry denied for contact"); + } + } + + return 0; // Unknown request or denied +} + +void CompanionMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) +{ + LOG_INF("onContactResponse: len=%d from contact", len); + if (len < 4) return; // Need at least 4-byte tag + + uint32_t tag; + memcpy(&tag, data, 4); + LOG_INF("onContactResponse: tag=%08x, _pending_login=%08x", tag, _pending_login); + + // Check for login response + if (_pending_login && memcmp(&_pending_login, contact.id.pub_key, 4) == 0) { + LOG_INF("onContactResponse: LOGIN RESPONSE MATCHED!"); + _pending_login = 0; + + uint8_t rsp[16]; + int i = 0; + if (len > 4 && memcmp(&data[4], "OK", 2) == 0) { + // Legacy Repeater login OK response + rsp[i++] = PUSH_CODE_LOGIN_SUCCESS; + rsp[i++] = 0; // legacy: is_admin = false + memcpy(&rsp[i], contact.id.pub_key, 6); + i += 6; + } else if (len > 4 && data[4] == RESP_SERVER_LOGIN_OK) { + // New login response format — start keepalive connection + if (len > 5) { + uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16; + if (keep_alive_secs > 0) { + startConnection(contact, keep_alive_secs); + } + } + rsp[i++] = PUSH_CODE_LOGIN_SUCCESS; + rsp[i++] = (len > 6) ? data[6] : 0; // permissions (is_admin) + memcpy(&rsp[i], contact.id.pub_key, 6); + i += 6; + memcpy(&rsp[i], &tag, 4); // server timestamp + i += 4; + if (len > 7) rsp[i++] = data[7]; // ACL permissions + else rsp[i++] = 0; + if (len > 12) rsp[i++] = data[12]; // FIRMWARE_VER_LEVEL + else rsp[i++] = 0; + } else { + // Login failed + rsp[i++] = PUSH_CODE_LOGIN_FAIL; + rsp[i++] = 0; // reserved + memcpy(&rsp[i], contact.id.pub_key, 6); + i += 6; + } + sendPush(rsp[0], &rsp[1], i - 1); + return; + } + + // Check for status response + if (_pending_status && len > 4 && memcmp(&_pending_status, contact.id.pub_key, 4) == 0) { + _pending_status = 0; + + uint8_t rsp[128]; + int i = 0; + rsp[i++] = PUSH_CODE_STATUS_RESPONSE; + rsp[i++] = 0; // reserved + memcpy(&rsp[i], contact.id.pub_key, 6); + i += 6; + int data_len = len - 4; + if (data_len > (int)sizeof(rsp) - i) data_len = sizeof(rsp) - i; + memcpy(&rsp[i], &data[4], data_len); + i += data_len; + sendPush(rsp[0], &rsp[1], i - 1); + return; + } + + // Check for telemetry response + if (len > 4 && tag == _pending_telemetry) { + _pending_telemetry = 0; + + uint8_t rsp[128]; + int i = 0; + rsp[i++] = PUSH_CODE_TELEMETRY_RESPONSE; + rsp[i++] = 0; // reserved + memcpy(&rsp[i], contact.id.pub_key, 6); + i += 6; + int data_len = len - 4; + if (data_len > (int)sizeof(rsp) - i) data_len = sizeof(rsp) - i; + memcpy(&rsp[i], &data[4], data_len); + i += data_len; + sendPush(rsp[0], &rsp[1], i - 1); + return; + } + + // Path discovery responses are now handled by onContactPathRecv() override, + // which has access to the actual in_path/out_path routing data. + + // Check for binary request response + if (len > 4 && tag == _pending_req) { + _pending_req = 0; + + uint8_t rsp[MAX_FRAME_SIZE]; + int i = 0; + rsp[i++] = PUSH_CODE_BINARY_RESPONSE; + rsp[i++] = 0; // reserved + // Arduino: sends tag (4 bytes) so app can match response to request + memcpy(&rsp[i], &tag, 4); + i += 4; + int data_len = len - 4; + if (data_len > (int)sizeof(rsp) - i) data_len = sizeof(rsp) - i; + memcpy(&rsp[i], &data[4], data_len); + i += data_len; + sendPush(rsp[0], &rsp[1], i - 1); + return; + } +} + +/* Raw packet logging - sends all RX packets to app for "heard X repeats" etc */ +void CompanionMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) +{ + // Arduino: PUSH_CODE_LOG_RX_DATA (0x88), snr*4, rssi, raw_bytes... + if (len + 3 > MAX_FRAME_SIZE) return; // buffer overflow protection + + uint8_t buf[MAX_FRAME_SIZE]; + int i = 0; + buf[i++] = PUSH_CODE_LOG_RX_DATA; + buf[i++] = (int8_t)(snr * 4); + buf[i++] = (int8_t)(rssi); + memcpy(&buf[i], raw, len); + i += len; + + sendPush(buf[0], &buf[1], i - 1); +} + +/* Trace path response - for ping/TRACE commands */ +void CompanionMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) +{ + LOG_INF("onTraceRecv: tag=0x%08x auth=0x%08x flags=0x%02x path_len=%d", + tag, auth_code, flags, path_len); + + // path_sz is encoded in flags bits 0-1 (0=1, 1=2, 2=4, 3=8 byte hash) + uint8_t path_sz = flags & 0x03; + // Calculate total size: 12 header + path_hashes + path_snrs + final SNR + size_t needed = 12 + path_len + (path_len >> path_sz) + 1; + if (needed > MAX_FRAME_SIZE) return; // buffer overflow protection + + uint8_t buf[MAX_FRAME_SIZE]; + int i = 0; + buf[i++] = PUSH_CODE_TRACE_DATA; + buf[i++] = 0; // reserved + buf[i++] = path_len; + buf[i++] = flags; + memcpy(&buf[i], &tag, 4); i += 4; + memcpy(&buf[i], &auth_code, 4); i += 4; + memcpy(&buf[i], path_hashes, path_len); i += path_len; + memcpy(&buf[i], path_snrs, path_len >> path_sz); i += (path_len >> path_sz); + buf[i++] = (int8_t)(packet->getSNR() * 4); + + sendPush(buf[0], &buf[1], i - 1); +} + +/* Control data response - for repeater discovery, etc */ +void CompanionMesh::onControlDataRecv(mesh::Packet *packet) +{ + LOG_INF("onControlDataRecv: payload_len=%d path_len=%d", packet->payload_len, packet->path_len); + + // Buffer: [PUSH_CODE_CONTROL_DATA][snr*4][rssi][path_len][payload...] + if (packet->payload_len + 4 > MAX_FRAME_SIZE) { + LOG_WRN("onControlDataRecv: payload too long (%d)", packet->payload_len); + return; + } + + uint8_t buf[MAX_FRAME_SIZE]; + int i = 0; + buf[i++] = PUSH_CODE_CONTROL_DATA; + buf[i++] = (int8_t)(packet->getSNR() * 4); + buf[i++] = (int8_t)(_radio->getLastRSSI()); + buf[i++] = packet->path_len; + memcpy(&buf[i], packet->payload, packet->payload_len); + i += packet->payload_len; + + sendPush(buf[0], &buf[1], i - 1); +} + +/* Raw data response - for custom packet types */ +void CompanionMesh::onRawDataRecv(mesh::Packet *packet) +{ + LOG_INF("onRawDataRecv: payload_len=%d", packet->payload_len); + + // Buffer: [PUSH_CODE_RAW_DATA][snr*4][rssi][reserved][payload...] + if (packet->payload_len + 4 > MAX_FRAME_SIZE) { + LOG_WRN("onRawDataRecv: payload too long (%d)", packet->payload_len); + return; + } + + uint8_t buf[MAX_FRAME_SIZE]; + int i = 0; + buf[i++] = PUSH_CODE_RAW_DATA; + buf[i++] = (int8_t)(packet->getSNR() * 4); + buf[i++] = (int8_t)(_radio->getLastRSSI()); + buf[i++] = 0xFF; // reserved (possibly path_len in future) + memcpy(&buf[i], packet->payload, packet->payload_len); + i += packet->payload_len; + + sendPush(buf[0], &buf[1], i - 1); +} + +/* Dispatcher tuning - uses prefs for configurable behavior */ +uint32_t CompanionMesh::getDirectRetransmitDelay(const mesh::Packet *packet) +{ + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * 0.3f); + return getRNG()->nextInt(0, 5 * t + 1); +} + +uint8_t CompanionMesh::getDutyCyclePercent() const +{ + return (uint8_t)prefs.airtime_factor; +} + +uint8_t CompanionMesh::getExtraAckTransmitCount() const +{ + return prefs.multi_acks; +} + +/* Auto-add filtering */ +bool CompanionMesh::isAutoAddEnabled() const +{ + return (prefs.manual_add_contacts & 1) == 0; +} + +bool CompanionMesh::shouldAutoAddContactType(uint8_t contact_type) const +{ + LOG_INF("shouldAutoAddContactType: type=%d manual_add=0x%02x autoadd_cfg=0x%02x", + contact_type, prefs.manual_add_contacts, prefs.autoadd_config); + + if ((prefs.manual_add_contacts & 1) == 0) { + LOG_INF("shouldAutoAddContactType: returning true (manual mode OFF)"); + return true; // Auto-add all when not in manual mode + } + + uint8_t type_bit = 0; + switch (contact_type) { + case ADV_TYPE_CHAT: + type_bit = AUTO_ADD_CHAT; + break; + case ADV_TYPE_REPEATER: + type_bit = AUTO_ADD_REPEATER; + break; + case ADV_TYPE_ROOM: + type_bit = AUTO_ADD_ROOM_SERVER; + break; + case ADV_TYPE_SENSOR: + type_bit = AUTO_ADD_SENSOR; + break; + default: + LOG_INF("shouldAutoAddContactType: unknown type, returning false"); + return false; + } + bool result = (prefs.autoadd_config & type_bit) != 0; + LOG_INF("shouldAutoAddContactType: type_bit=0x%02x result=%d", type_bit, result); + return result; +} + +bool CompanionMesh::shouldOverwriteWhenFull() const +{ + return (prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0; +} + +void CompanionMesh::onContactsFull() +{ + sendPush(PUSH_CODE_CONTACTS_FULL); +} + +void CompanionMesh::onContactOverwrite(const uint8_t *pub_key) +{ + _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); + sendPush(PUSH_CODE_CONTACT_DELETED, pub_key, PUB_KEY_SIZE); +} + +/* Advert path tracking — return the N most recently heard entries, + * sorted by recv_timestamp descending (newest first). + * The advert path table is a circular buffer, so we can't just + * take the first N entries — we must find the N with highest timestamps. */ +int CompanionMesh::getRecentlyHeard(AdvertPath dest[], int max_num) +{ + int count = 0; + + /* Collect all non-empty entries (up to max_num) keeping the + * most recent ones. Simple insertion: for each table entry, + * if the dest array isn't full, append; otherwise replace the + * oldest entry in dest if this one is newer. */ + for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { + if (_advert_paths[i].recv_timestamp == 0) { + continue; + } + if (count < max_num) { + dest[count++] = _advert_paths[i]; + } else { + /* Find oldest entry in dest */ + int oldest = 0; + for (int j = 1; j < count; j++) { + if (dest[j].recv_timestamp < dest[oldest].recv_timestamp) { + oldest = j; + } + } + /* Replace if this entry is newer */ + if (_advert_paths[i].recv_timestamp > dest[oldest].recv_timestamp) { + dest[oldest] = _advert_paths[i]; + } + } + } + + /* Sort descending by recv_timestamp (newest first) */ + for (int i = 0; i < count - 1; i++) { + for (int j = i + 1; j < count; j++) { + if (dest[j].recv_timestamp > dest[i].recv_timestamp) { + AdvertPath tmp = dest[i]; + dest[i] = dest[j]; + dest[j] = tmp; + } + } + } + + return count; +} + +const AdvertPath *CompanionMesh::findAdvertPath(const uint8_t *pubkey_prefix, int prefix_len) +{ + int match_len = (prefix_len < 7) ? prefix_len : 7; + for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { + if (_advert_paths[i].recv_timestamp != 0 && + memcmp(_advert_paths[i].pubkey_prefix, pubkey_prefix, match_len) == 0) { + return &_advert_paths[i]; + } + } + return nullptr; +} + +bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) +{ + if (len < 1) return false; + + /* Debug: log all incoming commands */ + LOG_DBG("CMD: 0x%02x len=%u", data[0], (unsigned)len); + + // Reset contact iterator when any new command is received (except during iteration) + if (data[0] != CMD_GET_CONTACTS && _contact_iter_active) { + resetContactIterator(); + } + + switch (data[0]) { + case CMD_APP_START: + if (len >= 8) { + // Reset contact iterator for fresh start + _contact_iter_active = false; + + // Return SELF_INFO + uint8_t rsp[90]; // 58 fixed + up to 32 bytes name + size_t i = 0; + rsp[i++] = PACKET_SELF_INFO; + rsp[i++] = ADV_TYPE_CHAT; + rsp[i++] = prefs.tx_power_dbm; + rsp[i++] = MAX_LORA_TX_POWER; // Max allowed TX power + memcpy(&rsp[i], self_id.pub_key, PUB_KEY_SIZE); i += PUB_KEY_SIZE; + int32_t lat = (int32_t)(prefs.node_lat * 1000000.0); + int32_t lon = (int32_t)(prefs.node_lon * 1000000.0); + LOG_INF("SELF_INFO: lat=%d lon=%d advert_loc=%d multi_acks=%d", + lat, lon, prefs.advert_loc_policy, prefs.multi_acks); + put_le32(&rsp[i], lat); i += 4; + put_le32(&rsp[i], lon); i += 4; + rsp[i++] = prefs.multi_acks; + rsp[i++] = prefs.advert_loc_policy; + LOG_INF("SELF_INFO bytes[36-45]: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x", + rsp[36], rsp[37], rsp[38], rsp[39], rsp[40], rsp[41], rsp[42], rsp[43], rsp[44], rsp[45]); + // Telemetry modes: (env << 4) | (loc << 2) | base + rsp[i++] = (prefs.telemetry_mode_env << 4) | (prefs.telemetry_mode_loc << 2) | prefs.telemetry_mode_base; + rsp[i++] = prefs.manual_add_contacts; + put_le32(&rsp[i], (uint32_t)(prefs.freq * 1000)); i += 4; + put_le32(&rsp[i], (uint32_t)(prefs.bw * 1000)); i += 4; + rsp[i++] = prefs.sf; + rsp[i++] = prefs.cr; + size_t name_len = strnlen(prefs.node_name, sizeof(prefs.node_name) - 1); + memcpy(&rsp[i], prefs.node_name, name_len); + i += name_len; + writeFrame(rsp, i); + return true; + } + break; + + case CMD_GET_CONTACTS: + if (_contact_iter_active) { + sendPacketError(ERR_BAD_STATE); + } else { + // Parse optional 'since' parameter for incremental sync + if (len >= 5) { + _contact_iter_since = get_le32(&data[1]); + } else { + _contact_iter_since = 0; + } + + // Send PACKET_CONTACT_START with total count (unfiltered) + uint8_t rsp[5]; + rsp[0] = PACKET_CONTACT_START; + put_le32(&rsp[1], (uint32_t)getNumContacts()); + writeFrame(rsp, sizeof(rsp)); + _contact_iter_idx = 0; + _contact_iter_lastmod = 0; + _contact_iter_active = true; + } + return true; + + case CMD_ADD_UPDATE_CONTACT: + if (len >= 1 + PUB_KEY_SIZE + 1 + 1 + 1 + MAX_PATH_SIZE + 32 + 4) { + const uint8_t *pub_key = &data[1]; + ContactInfo *existing = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + + ContactInfo c; + size_t i = 1; + memcpy(c.id.pub_key, &data[i], PUB_KEY_SIZE); i += PUB_KEY_SIZE; + c.type = data[i++]; + c.flags = data[i++]; + c.out_path_len = (int8_t)data[i++]; + memcpy(c.out_path, &data[i], MAX_PATH_SIZE); i += MAX_PATH_SIZE; + memcpy(c.name, &data[i], 32); i += 32; + c.last_advert_timestamp = get_le32(&data[i]); i += 4; + c.gps_lat = 0; + c.gps_lon = 0; + c.lastmod = (uint32_t)getRTCClock()->getCurrentTime(); + if (len >= i + 8) { + c.gps_lat = (int32_t)get_le32(&data[i]); i += 4; + c.gps_lon = (int32_t)get_le32(&data[i]); i += 4; + if (len >= i + 4) { + c.lastmod = get_le32(&data[i]); + } + } + c.sync_since = 0; + c.shared_secret_valid = false; + + if (existing) { + // Update existing + *existing = c; + markContactsDirty(); + sendPacketOk(); + } else if (addContact(c)) { + markContactsDirty(); + sendPacketOk(); + } else { + sendPacketError(ERR_TABLE_FULL); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_REMOVE_CONTACT: + if (len >= 1 + PUB_KEY_SIZE) { + ContactInfo *c = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + if (c && removeContact(*c)) { + _store->deleteBlobByKey(&data[1], PUB_KEY_SIZE); + markContactsDirty(); + sendPacketOk(); + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_RESET_PATH: + if (len >= 1 + PUB_KEY_SIZE) { + ContactInfo *c = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + if (c) { + resetPathTo(*c); + markContactsDirty(); + sendPacketOk(); + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_GET_CONTACT_BY_KEY: + if (len >= 1 + PUB_KEY_SIZE) { + ContactInfo *c = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + if (c) { + uint8_t rsp[CONTACT_FRAME_SIZE]; + size_t n = serializeContact(rsp, *c, PACKET_CONTACT); + writeFrame(rsp, n); + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_GET_CHANNEL: + // Format: [cmd][channel_idx] + // Response: [code][idx][32 name][16 secret] = 50 bytes (matches Arduino) + // Arduino returns channel info even if slot is empty (name[0]=='\0') + if (len >= 2 && data[1] < MAX_GROUP_CHANNELS) { + ChannelDetails ch; + if (getChannel(data[1], ch)) { + uint8_t rsp[50]; + int i = 0; + rsp[i++] = PACKET_CHANNEL_INFO; + rsp[i++] = data[1]; + memcpy(&rsp[i], ch.name, 32); + i += 32; + memcpy(&rsp[i], ch.channel.secret, 16); // return 128-bit secret + i += 16; + writeFrame(rsp, i); + } else { + sendPacketError(ERR_NOT_FOUND); // Only if index out of range + } + return true; + } + break; + + case CMD_SET_CHANNEL: + // Format: [cmd][idx][32 name][16 or 32 secret] + // Arduino rejects 32-byte secrets, but we accept 16-byte (50 bytes total) + if (len >= 2 + 32 + 32 && data[1] < MAX_GROUP_CHANNELS) { + // 32-byte secret not supported (matches Arduino) + sendPacketError(ERR_UNSUPPORTED); + return true; + } + if (len >= 2 + 32 + 16 && data[1] < MAX_GROUP_CHANNELS) { + ChannelDetails ch; + // Copy name (null-terminate if needed) + memcpy(ch.name, &data[2], 32); + ch.name[31] = '\0'; // ensure null termination + // Copy 16-byte secret, zero upper 16 bytes + memset(ch.channel.secret, 0, sizeof(ch.channel.secret)); + memcpy(ch.channel.secret, &data[2 + 32], 16); + // setChannel computes the hash based on whether upper 16 bytes are zero + if (setChannel(data[1], ch)) { + markChannelsDirty(); + sendPacketOk(); + } else { + sendPacketError(ERR_NOT_FOUND); // bad channel_idx + } + return true; + } + break; + + case CMD_GET_BATT_AND_STORAGE: { + uint8_t rsp[11]; + rsp[0] = PACKET_BATTERY; + put_le16(&rsp[1], _batt_cb ? _batt_cb() : 0); + put_le32(&rsp[3], _store->getStorageUsedKb()); + put_le32(&rsp[7], _store->getStorageTotalKb()); + writeFrame(rsp, sizeof(rsp)); + return true; + } + + case CMD_GET_STATS: + if (len >= 2) { + uint8_t stats_type = data[1]; + if (stats_type == 0) { + // STATS_TYPE_CORE: battery_mv(2) + uptime_secs(4) + err_flags(2) + queue_len(1) + uint8_t rsp[11]; + size_t i = 0; + rsp[i++] = PACKET_STATS; + rsp[i++] = 0; // STATS_TYPE_CORE + put_le16(&rsp[i], _batt_cb ? _batt_cb() : 0); i += 2; + put_le32(&rsp[i], (uint32_t)(_ms->getMillis() / 1000)); i += 4; // uptime_secs + put_le16(&rsp[i], getErrFlags()); i += 2; + rsp[i++] = (uint8_t)_mgr->getOutboundCount((uint32_t)_ms->getMillis()); + writeFrame(rsp, i); + } else if (stats_type == 1) { + // STATS_TYPE_RADIO: noise_floor(2) + last_rssi(1) + last_snr(1) + tx_air_secs(4) + rx_air_secs(4) + uint8_t rsp[14]; + size_t i = 0; + rsp[i++] = PACKET_STATS; + rsp[i++] = 1; // STATS_TYPE_RADIO + put_le16(&rsp[i], (int16_t)_radio->getNoiseFloor()); i += 2; + rsp[i++] = (int8_t)_radio->getLastRSSI(); + rsp[i++] = (int8_t)(_radio->getLastSNR() * 4); // scaled by 4 + put_le32(&rsp[i], getTotalAirTime() / 1000); i += 4; // tx_air_secs + put_le32(&rsp[i], getReceiveAirTime() / 1000); i += 4; // rx_air_secs + writeFrame(rsp, i); + } else if (stats_type == 2) { + // STATS_TYPE_PACKETS: recv(4) + sent(4) + sent_flood(4) + sent_direct(4) + recv_flood(4) + recv_direct(4) + recv_errors(4) + uint8_t rsp[30]; + size_t i = 0; + rsp[i++] = PACKET_STATS; + rsp[i++] = 2; // STATS_TYPE_PACKETS + uint32_t sent_flood = getNumSentFlood(); + uint32_t sent_direct = getNumSentDirect(); + uint32_t recv_flood = getNumRecvFlood(); + uint32_t recv_direct = getNumRecvDirect(); + put_le32(&rsp[i], recv_flood + recv_direct); i += 4; // total recv + put_le32(&rsp[i], sent_flood + sent_direct); i += 4; // total sent + put_le32(&rsp[i], sent_flood); i += 4; + put_le32(&rsp[i], sent_direct); i += 4; + put_le32(&rsp[i], recv_flood); i += 4; + put_le32(&rsp[i], recv_direct); i += 4; + put_le32(&rsp[i], _radio->getPacketsRecvErrors()); i += 4; + writeFrame(rsp, i); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SHARE_CONTACT: + if (len >= 1 + PUB_KEY_SIZE) { + ContactInfo *c = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + if (c && shareContactZeroHop(*c)) { + sendPacketOk(); + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_EXPORT_CONTACT: + if (len < 1 + PUB_KEY_SIZE) { + // Export SELF - create self advert packet + mesh::Packet *pkt; + if (prefs.advert_loc_policy == 0) { // ADVERT_LOC_NONE + pkt = createSelfAdvert(prefs.node_name); + } else { + pkt = createSelfAdvert(prefs.node_name, prefs.node_lat, prefs.node_lon); + } + if (pkt) { + pkt->header |= ROUTE_TYPE_FLOOD; + uint8_t rsp[128]; + rsp[0] = PACKET_EXPORT_CONTACT; + uint8_t out_len = pkt->writeTo(&rsp[1]); + releasePacket(pkt); + writeFrame(rsp, out_len + 1); + } else { + sendPacketError(ERR_TABLE_FULL); + } + } else { + // Export specific contact by pubkey + ContactInfo *c = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + if (c) { + uint8_t rsp[128]; + rsp[0] = PACKET_EXPORT_CONTACT; + uint8_t export_len = exportContact(*c, &rsp[1]); + if (export_len > 0) { + writeFrame(rsp, export_len + 1); + } else { + sendPacketError(ERR_BAD_STATE); + } + } else { + sendPacketError(ERR_NOT_FOUND); + } + } + return true; + + case CMD_IMPORT_CONTACT: + // Arduino: len > 2 + 32 + 64 = 98 bytes (header + pubkey + signature) + if (len > 2 + 32 + 64) { + if (importContact(&data[1], len - 1)) { + markContactsDirty(); + sendPacketOk(); + } else { + sendPacketError(ERR_ILLEGAL_ARG); // Match Arduino error code + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SEND_TXT_MSG: + // Frame format: cmd(1) + txt_type(1) + attempt(1) + timestamp(4) + pub_key_prefix(6) + text(N) + // Minimum: 1 + 1 + 1 + 4 + 6 + 1 = 14 bytes + LOG_INF("CMD_SEND_TXT_MSG: len=%u (min=14)", (unsigned)len); + if (len >= 14) { + int i = 1; + uint8_t txt_type = data[i++]; + uint8_t attempt = data[i++]; + uint32_t msg_timestamp = get_le32(&data[i]); i += 4; + const uint8_t *pub_key_prefix = &data[i]; i += 6; + + // Copy text and ensure null termination + size_t text_len = len - 13; // Everything after header (13 bytes) + char text_buf[MAX_TEXT_LEN + 1]; + if (text_len > MAX_TEXT_LEN) text_len = MAX_TEXT_LEN; + memcpy(text_buf, &data[i], text_len); + text_buf[text_len] = '\0'; + const char *text = text_buf; + + LOG_INF("CMD_SEND_TXT_MSG: txt_type=%d attempt=%d pubkey=%02x%02x%02x%02x%02x%02x", + txt_type, attempt, pub_key_prefix[0], pub_key_prefix[1], pub_key_prefix[2], + pub_key_prefix[3], pub_key_prefix[4], pub_key_prefix[5]); + + ContactInfo *contact = lookupContactByPubKey(pub_key_prefix, 6); + if (contact && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) { + LOG_INF("CMD_SEND_TXT_MSG: contact='%s' text='%s' text_len=%u", contact->name, text, (unsigned)text_len); + + uint32_t expected_ack = 0, est_timeout; + int result; + + if (txt_type == TXT_TYPE_CLI_DATA) { + // Use node's RTC instead of app timestamp + msg_timestamp = getRTCClock()->getCurrentTimeUnique(); + result = sendCommandData(*contact, msg_timestamp, attempt, text, est_timeout); + } else { + result = sendMessage(*contact, msg_timestamp, attempt, text, expected_ack, est_timeout); + } + + LOG_INF("CMD_SEND_TXT_MSG: sendMessage result=%d expected_ack=0x%08x", result, expected_ack); + + if (result != MSG_SEND_FAILED) { + if (expected_ack) { + // Track ACK + int idx = -1; + ContactInfo ci; + for (int k = 0; k < getNumContacts(); k++) { + if (getContactByIdx(k, ci) && ci.id.matches(contact->id)) { + idx = k; + break; + } + } + addPendingAck(expected_ack, idx); + } + + // Response: RESP_CODE_SENT + is_flood(1) + expected_ack(4) + est_timeout(4) + uint8_t rsp[10]; + rsp[0] = PACKET_SENT; + rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + put_le32(&rsp[2], expected_ack); + put_le32(&rsp[6], est_timeout); + writeFrame(rsp, sizeof(rsp)); + } else { + sendPacketError(ERR_TABLE_FULL); + } + } else { + LOG_WRN("CMD_SEND_TXT_MSG: contact not found or unsupported txt_type=%d", txt_type); + sendPacketError(contact == nullptr ? ERR_NOT_FOUND : ERR_UNSUPPORTED); + } + return true; + } + LOG_WRN("CMD_SEND_TXT_MSG: frame too short (len=%u, need 14)", (unsigned)len); + sendPacketError(ERR_ILLEGAL_ARG); + return true; + + case CMD_SEND_CHANNEL_TXT_MSG: + // Arduino format: [cmd][txt_type][channel_idx][4-byte timestamp][text...] + // Minimum: 1 + 1 + 1 + 4 + 1 = 8 bytes + if (len >= 8) { + int i = 1; + uint8_t txt_type = data[i++]; + uint8_t ch_idx = data[i++]; + uint32_t msg_timestamp = get_le32(&data[i]); + i += 4; + const char *text = (const char *)&data[i]; + size_t text_len = len - i; + + if (txt_type != TXT_TYPE_PLAIN) { + sendPacketError(ERR_UNSUPPORTED); + } else { + ChannelDetails ch; + if (getChannel(ch_idx, ch)) { + if (sendGroupMessage(msg_timestamp, ch.channel, prefs.node_name, text, text_len)) { + sendPacketOk(); + } else { + sendPacketError(ERR_BAD_STATE); + } + } else { + sendPacketError(ERR_NOT_FOUND); + } + } + return true; + } + break; + + case CMD_SEND_SELF_ADVERT: { + mesh::Packet *adv; + if (prefs.advert_loc_policy == 0) { /* ADVERT_LOC_NONE */ + adv = createSelfAdvert(prefs.node_name); + } else { + adv = createSelfAdvert(prefs.node_name, prefs.node_lat, prefs.node_lon); + } + if (adv) { + /* Optional param: data[1] == 1 means flood, else zero-hop */ + if (len >= 2 && data[1] == 1) { + sendFlood(adv); + } else { + sendZeroHop(adv); + } + sendPacketOk(); + } else { + sendPacketError(ERR_TABLE_FULL); + } + return true; + } + + case CMD_SYNC_NEXT_MESSAGE: { + LOG_INF("CMD_SYNC_NEXT_MESSAGE: queue_count=%d", _offline_queue_count); + uint8_t buf[MAX_FRAME_SIZE]; + size_t msg_len; + if (dequeueOfflineMessage(buf, msg_len)) { + LOG_INF("CMD_SYNC_NEXT_MESSAGE: dequeued msg_len=%u type=0x%02x", (unsigned)msg_len, buf[0]); + writeFrame(buf, msg_len); + } else { + LOG_INF("CMD_SYNC_NEXT_MESSAGE: queue empty, sending NO_MORE_MSGS"); + uint8_t rsp[] = { PACKET_NO_MORE_MSGS }; + writeFrame(rsp, sizeof(rsp)); + } + return true; + } + + case CMD_SET_ADVERT_NAME: + if (len >= 2) { + size_t nlen = len - 1; + if (nlen >= sizeof(prefs.node_name)) nlen = sizeof(prefs.node_name) - 1; + memcpy(prefs.node_name, &data[1], nlen); + prefs.node_name[nlen] = '\0'; + _store->savePrefs(prefs); + } + sendPacketOk(); + return true; + + case CMD_SET_RADIO_PARAMS: + if (len >= 11) { + uint32_t freq = get_le32(&data[1]); + uint32_t bw = get_le32(&data[5]); + uint8_t sf = data[9]; + uint8_t cr = data[10]; + uint8_t repeat = 0; + if (len > 11) { + repeat = data[11]; // v9+: client repeat flag + } + // If repeat requested, validate frequency against allowed bands + if (repeat && !isValidClientRepeatFreq(freq)) { + sendPacketError(ERR_ILLEGAL_ARG); + } else if (freq >= 300000 && freq <= 2500000 && + bw >= 7000 && bw <= 500000 && + sf >= 5 && sf <= 12 && + cr >= 5 && cr <= 8) { + prefs.freq = (float)freq / 1000.0f; + prefs.bw = (float)bw / 1000.0f; + prefs.sf = sf; + prefs.cr = cr; + prefs.client_repeat = repeat; + _store->savePrefs(prefs); + if (_radio_reconfig_cb) _radio_reconfig_cb(); + LOG_INF("SET_RADIO_PARAMS: client_repeat=%d", repeat); + sendPacketOk(); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SET_RADIO_TX_POWER: + if (len >= 2) { + int8_t power = (int8_t)data[1]; + if (power >= -9 && power <= MAX_LORA_TX_POWER) { + prefs.tx_power_dbm = (uint8_t)power; + _store->savePrefs(prefs); + if (_radio_reconfig_cb) _radio_reconfig_cb(); + sendPacketOk(); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SET_ADVERT_LATLON: + if (len >= 9) { + int32_t lat = (int32_t)get_le32(&data[1]); + int32_t lon = (int32_t)get_le32(&data[5]); + // Arduino validation: lat ±90°, lon ±180° (in microdegrees) + if (lat >= -90000000 && lat <= 90000000 && + lon >= -180000000 && lon <= 180000000) { + prefs.node_lat = (double)lat / 1000000.0; + prefs.node_lon = (double)lon / 1000000.0; + _store->savePrefs(prefs); + LOG_INF("SET_ADVERT_LATLON: lat=%d lon=%d policy=%d", + lat, lon, prefs.advert_loc_policy); + sendPacketOk(); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_GET_DEVICE_TIME: { + uint8_t rsp[5]; + rsp[0] = PACKET_CURR_TIME; + put_le32(&rsp[1], (uint32_t)getRTCClock()->getCurrentTime()); + writeFrame(rsp, sizeof(rsp)); + return true; + } + + case CMD_SET_DEVICE_TIME: + if (len >= 5) { + /* If we have GPS-synced time, ignore phone time sync attempts. + * GPS time is more accurate than phone time. Still return OK + * so the app doesn't keep retrying. */ + if (gps_has_time_sync()) { + LOG_INF("Ignoring phone time sync - GPS time sync active"); + sendPacketOk(); + return true; + } + + uint32_t secs = get_le32(&data[1]); + uint32_t curr = getRTCClock()->getCurrentTime(); + // Arduino: only allow setting time forward (prevents time attacks) + if (secs >= curr) { + getRTCClock()->setCurrentTime(secs); + sendPacketOk(); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_DEVICE_QUERY: + if (len >= 2) { + _app_target_ver = data[1]; // Which version of protocol does app understand + #ifndef FIRMWARE_BUILD_DATE + #define FIRMWARE_BUILD_DATE __DATE__ + #endif + static const uint8_t fw_build[12] = FIRMWARE_BUILD_DATE; + static const uint8_t model[40] = CONFIG_ZEPHCORE_BOARD_NAME; + static const uint8_t version[20] = "v1.13.0-zephyr"; + uint8_t rsp[81]; + rsp[0] = PACKET_DEVICE_INFO; + rsp[1] = 9; // FIRMWARE_VER_CODE - v9 = client_repeat support + rsp[2] = MAX_CONTACTS / 2; + rsp[3] = MAX_GROUP_CHANNELS; + put_le32(&rsp[4], prefs.ble_pin ? prefs.ble_pin : 123456); // BLE PIN + memcpy(&rsp[8], fw_build, 12); + memcpy(&rsp[20], model, 40); + memcpy(&rsp[60], version, 20); + rsp[80] = prefs.client_repeat; // v9+: offgrid mode state + writeFrame(rsp, sizeof(rsp)); + return true; + } + break; + + case CMD_GET_TUNING_PARAMS: { + uint8_t rsp[9]; + rsp[0] = PACKET_TUNING_PARAMS; + put_le32(&rsp[1], (uint32_t)(prefs.rx_delay_base * 1000)); + put_le32(&rsp[5], (uint32_t)(prefs.airtime_factor * 1000)); + writeFrame(rsp, sizeof(rsp)); + return true; + } + + case CMD_SET_TUNING_PARAMS: + if (len >= 9) { + prefs.rx_delay_base = (float)get_le32(&data[1]) / 1000.0f; + prefs.airtime_factor = (float)get_le32(&data[5]) / 1000.0f; + _store->savePrefs(prefs); + } + sendPacketOk(); + return true; + + case CMD_EXPORT_PRIVATE_KEY: { + // Response: [PACKET_PRIVATE_KEY=0x0E][64 bytes: 32 private + 32 public] + uint8_t rsp[65]; + rsp[0] = PACKET_PRIVATE_KEY; + self_id.writeTo(&rsp[1], 64); + writeFrame(rsp, sizeof(rsp)); + return true; + } + + case CMD_FACTORY_RESET: + // Arduino: requires "reset" magic string for safety + if (len >= 6 && memcmp(&data[1], "reset", 5) == 0) { + LOG_INF("Factory reset requested"); + _store->factoryReset(); + sendPacketOk(); + // Reboot to regenerate identity + sys_reboot(SYS_REBOOT_COLD); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_REBOOT: + if (len >= 7 && memcmp(&data[1], "reboot", 6) == 0) { + LOG_INF("Reboot requested"); + /* Flush any pending lazy writes before reboot */ + flushDirtyContacts(); + flushDirtyChannels(); + sendPacketOk(); + sys_reboot(SYS_REBOOT_COLD); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SET_OTHER_PARAMS: + if (len >= 2) { + prefs.manual_add_contacts = data[1]; + if (len >= 3) { + // Telemetry modes: (env << 4) | (loc << 2) | base + prefs.telemetry_mode_base = data[2] & 0x03; + prefs.telemetry_mode_loc = (data[2] >> 2) & 0x03; + prefs.telemetry_mode_env = (data[2] >> 4) & 0x03; + if (len >= 4) { + prefs.advert_loc_policy = data[3]; + LOG_INF("SET_OTHER_PARAMS: advert_loc_policy=%d", prefs.advert_loc_policy); + if (len >= 5) { + prefs.multi_acks = data[4]; + } + } + } + _store->savePrefs(prefs); + } + sendPacketOk(); + return true; + + case CMD_SET_DEVICE_PIN: + if (len >= 5) { + uint32_t pin; + memcpy(&pin, &data[1], 4); + if (pin == 0 || (pin >= 100000 && pin <= 999999)) { + prefs.ble_pin = pin; + _store->savePrefs(prefs); + if (_pin_change_cb) _pin_change_cb(pin); + sendPacketOk(); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SIGN_START: + // Free any previous signing state + if (_sign_data) { + delete[] _sign_data; + _sign_data = nullptr; + } + _sign_data_len = 0; + _sign_data_capacity = 0; + // Allocate new buffer + _sign_data = new uint8_t[MAX_SIGN_DATA_LEN]; + if (_sign_data) { + _sign_data_capacity = MAX_SIGN_DATA_LEN; + // Response: [code][reserved][4-byte max_len] = 6 bytes (matches Arduino) + uint8_t rsp[6]; + rsp[0] = PACKET_SIGN_START; + rsp[1] = 0; // reserved + uint32_t max_len = MAX_SIGN_DATA_LEN; + put_le32(&rsp[2], max_len); + writeFrame(rsp, sizeof(rsp)); + } else { + sendPacketError(ERR_BAD_STATE); + } + return true; + + case CMD_SIGN_DATA: + if (_sign_data && len > 1) { + size_t chunk_len = len - 1; + if (_sign_data_len + chunk_len <= _sign_data_capacity) { + memcpy(&_sign_data[_sign_data_len], &data[1], chunk_len); + _sign_data_len += chunk_len; + sendPacketOk(); + } else { + sendPacketError(ERR_TABLE_FULL); + } + } else { + sendPacketError(ERR_BAD_STATE); + } + return true; + + case CMD_SIGN_FINISH: + if (_sign_data && _sign_data_len > 0) { + uint8_t signature[64]; + // Sign the data using Ed25519 via LocalIdentity method + self_id.sign(signature, _sign_data, _sign_data_len); + uint8_t rsp[1 + 64]; + rsp[0] = PACKET_SIGNATURE; + memcpy(&rsp[1], signature, 64); + writeFrame(rsp, sizeof(rsp)); + // Clean up + delete[] _sign_data; + _sign_data = nullptr; + _sign_data_len = 0; + _sign_data_capacity = 0; + } else { + sendPacketError(ERR_BAD_STATE); + } + return true; + + case CMD_SEND_TRACE_PATH: + /* Trace path: [cmd][4-byte tag][4-byte auth][flags][path...] */ + if (len > 10 && (len - 10) < MAX_PACKET_PAYLOAD - 5) { + uint8_t path_len = len - 10; + uint8_t flags = data[9]; + uint8_t path_sz = flags & 0x03; /* Path hash size encoding */ + /* Validate path length is multiple of hash size */ + if ((path_len >> path_sz) > MAX_PATH_SIZE || (path_len % (1 << path_sz)) != 0) { + sendPacketError(ERR_ILLEGAL_ARG); + } else { + uint32_t tag = get_le32(&data[1]); + uint32_t auth_code = get_le32(&data[5]); + mesh::Packet *trace = createTrace(tag, auth_code, flags); + if (trace) { + sendDirect(trace, &data[10], path_len); + + uint32_t airtime = _radio->getEstAirtimeFor(trace->payload_len + trace->path_len + 2); + uint32_t est_timeout = calcDirectTimeoutMillisFor(airtime, path_len >> path_sz); + + uint8_t rsp[10]; + rsp[0] = PACKET_SENT; + rsp[1] = 0; /* Not flood */ + put_le32(&rsp[2], tag); + put_le32(&rsp[6], est_timeout); + writeFrame(rsp, sizeof(rsp)); + } else { + sendPacketError(ERR_TABLE_FULL); + } + } + } else if (len >= 9) { + /* Legacy: zero-hop trace (backwards compat) */ + uint32_t tag = get_le32(&data[1]); + uint32_t auth_code = get_le32(&data[5]); + mesh::Packet *trace = createTrace(tag, auth_code); + if (trace) { + sendZeroHop(trace); + sendPacketOk(); + } else { + sendPacketError(ERR_BAD_STATE); + } + } else { + sendPacketOk(); /* Backwards compat */ + } + return true; + + case CMD_IMPORT_PRIVATE_KEY: + /* Import private key: [cmd][64 bytes prv_key] */ + if (len >= 1 + PRV_KEY_SIZE) { + if (!mesh::LocalIdentity::validatePrivateKey(&data[1])) { + sendPacketError(ERR_ILLEGAL_ARG); /* invalid key */ + } else { + mesh::LocalIdentity new_identity; + new_identity.readFrom(&data[1], PRV_KEY_SIZE); + if (_store->saveMainIdentity(new_identity)) { + self_id = new_identity; + /* Reload contacts to invalidate ECDH shared secrets */ + resetContacts(); + _store->loadContacts(this); + sendPacketOk(); + } else { + sendPacketError(ERR_BAD_STATE); + } + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SEND_RAW_DATA: + /* Raw data packet: [cmd][path_len][path...][payload...] */ + if (len >= 6) { /* min: cmd + path_len + 4 byte payload */ + int i = 1; + int8_t path_len = (int8_t)data[i++]; + if (path_len >= 0 && i + path_len + 4 <= (int)len) { + const uint8_t *path = &data[i]; + i += path_len; + mesh::Packet *pkt = createRawData(&data[i], len - i); + if (pkt) { + sendDirect(pkt, path, path_len); + sendPacketOk(); + } else { + sendPacketError(ERR_TABLE_FULL); + } + } else { + /* Flood mode not supported (path_len < 0) */ + sendPacketError(ERR_UNSUPPORTED); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SEND_LOGIN: + if (len >= 1 + PUB_KEY_SIZE) { + ContactInfo *contact = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + size_t pw_len = len - 1 - PUB_KEY_SIZE; + char pw_buf[64]; + if (pw_len >= sizeof(pw_buf)) pw_len = sizeof(pw_buf) - 1; + memcpy(pw_buf, &data[1 + PUB_KEY_SIZE], pw_len); + pw_buf[pw_len] = '\0'; + const char *password = pw_buf; + if (contact) { + uint32_t est_timeout; + LOG_INF("CMD_SEND_LOGIN: sending to contact, path_len=%d", contact->out_path_len); + int result = sendLogin(*contact, password, est_timeout); + LOG_INF("CMD_SEND_LOGIN: sendLogin returned %d, est_timeout=%u", result, est_timeout); + if (result != MSG_SEND_FAILED) { + clearPendingReqs(); + memcpy(&_pending_login, contact->id.pub_key, 4); // match in onContactResponse() + LOG_INF("CMD_SEND_LOGIN: _pending_login set to %08x", _pending_login); + uint8_t rsp[10]; + rsp[0] = PACKET_SENT; + rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&rsp[2], &_pending_login, 4); + put_le32(&rsp[6], est_timeout); + writeFrame(rsp, sizeof(rsp)); + } else { + sendPacketError(ERR_TABLE_FULL); + } + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SEND_STATUS_REQ: + if (len >= 1 + PUB_KEY_SIZE) { + ContactInfo *contact = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + if (contact) { + uint32_t tag, est_timeout; + int result = sendRequest(*contact, REQ_TYPE_GET_STATUS, tag, est_timeout); + if (result != MSG_SEND_FAILED) { + clearPendingReqs(); + memcpy(&_pending_status, contact->id.pub_key, 4); // legacy matching scheme + uint8_t rsp[10]; + rsp[0] = PACKET_SENT; + rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + put_le32(&rsp[2], tag); + put_le32(&rsp[6], est_timeout); + writeFrame(rsp, sizeof(rsp)); + } else { + sendPacketError(ERR_BAD_STATE); + } + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_HAS_CONNECTION: + if (len >= 1 + PUB_KEY_SIZE) { + if (hasConnectionTo(&data[1])) { + sendPacketOk(); + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_LOGOUT: + if (len >= 1 + PUB_KEY_SIZE) { + stopConnection(&data[1]); + } + sendPacketOk(); + return true; + + case CMD_SEND_TELEMETRY_REQ: + if (len == 4) { + // Self-telemetry request: return battery, GPS, and environment data + // Format: Cayenne LPP: [channel][type][data...] + // Response: [PUSH_CODE_TELEMETRY_RESPONSE][reserved][6-byte pubkey][telemetry_data] + uint8_t rsp[96]; // header(8)+batt(4)+gps(11)+env(11)+pwr(36)=70 worst case + int i = 0; + rsp[i++] = PUSH_CODE_TELEMETRY_RESPONSE; + rsp[i++] = 0; // reserved + memcpy(&rsp[i], self_id.pub_key, 6); + i += 6; // pubkey prefix + + // Channel 1 = TELEM_CHANNEL_SELF + const uint8_t CH_SELF = 1; + + // Battery voltage: [channel][LPP_VOLTAGE=116][2-byte 0.01V big-endian] + uint16_t batt_mv = _batt_cb ? _batt_cb() : 0; + rsp[i++] = CH_SELF; + rsp[i++] = 116; // LPP_VOLTAGE + uint16_t batt_scaled = batt_mv / 10; // 0.01V resolution + rsp[i++] = (batt_scaled >> 8) & 0xFF; // Big-endian + rsp[i++] = batt_scaled & 0xFF; + + // GPS position if available: [channel][LPP_GPS=136][3-byte lat][3-byte lon][3-byte alt] + // Use last known position even when GPS is sleeping (power-save mode) + struct gps_position pos; + if (gps_is_available() && gps_get_last_known_position(&pos)) { + rsp[i++] = CH_SELF; + rsp[i++] = 136; // LPP_GPS + // Lat/lon in 0.0001 degrees (signed 24-bit) + int32_t lat = (int32_t)(pos.latitude_ndeg / 100000); // nano-degrees to 0.0001 degrees + int32_t lon = (int32_t)(pos.longitude_ndeg / 100000); + int32_t alt = pos.altitude_mm / 10; // mm to 0.01m + rsp[i++] = (lat >> 16) & 0xFF; + rsp[i++] = (lat >> 8) & 0xFF; + rsp[i++] = lat & 0xFF; + rsp[i++] = (lon >> 16) & 0xFF; + rsp[i++] = (lon >> 8) & 0xFF; + rsp[i++] = lon & 0xFF; + rsp[i++] = (alt >> 16) & 0xFF; + rsp[i++] = (alt >> 8) & 0xFF; + rsp[i++] = alt & 0xFF; + } else if (prefs.node_lat != 0 || prefs.node_lon != 0) { + // Use configured position + rsp[i++] = CH_SELF; + rsp[i++] = 136; // LPP_GPS + int32_t lat = (int32_t)(prefs.node_lat * 10000); + int32_t lon = (int32_t)(prefs.node_lon * 10000); + int32_t alt = 0; + rsp[i++] = (lat >> 16) & 0xFF; + rsp[i++] = (lat >> 8) & 0xFF; + rsp[i++] = lat & 0xFF; + rsp[i++] = (lon >> 16) & 0xFF; + rsp[i++] = (lon >> 8) & 0xFF; + rsp[i++] = lon & 0xFF; + rsp[i++] = (alt >> 16) & 0xFF; + rsp[i++] = (alt >> 8) & 0xFF; + rsp[i++] = alt & 0xFF; + } + + // Environment sensors + { + struct env_data env; + if (env_sensors_read(&env) == 0) { + // Temperature: [channel][LPP_TEMPERATURE=103][2-byte 0.1C signed big-endian] + if (env.has_temperature) { + rsp[i++] = CH_SELF; + rsp[i++] = LPP_TEMPERATURE; + int16_t temp = (int16_t)(env.temperature_c * 10); + rsp[i++] = (temp >> 8) & 0xFF; + rsp[i++] = temp & 0xFF; + } else if (env.has_mcu_temperature) { + // MCU die temp as fallback when no external sensor + rsp[i++] = CH_SELF; + rsp[i++] = LPP_TEMPERATURE; + int16_t temp = (int16_t)(env.mcu_temperature_c * 10); + rsp[i++] = (temp >> 8) & 0xFF; + rsp[i++] = temp & 0xFF; + } + // Humidity: [channel][LPP_RELATIVE_HUMIDITY=104][1-byte 0.5%] + if (env.has_humidity) { + rsp[i++] = CH_SELF; + rsp[i++] = LPP_RELATIVE_HUMIDITY; + rsp[i++] = (uint8_t)(env.humidity_pct * 2); + } + // Pressure: [channel][LPP_BAROMETRIC_PRESSURE=115][2-byte 0.1hPa big-endian] + if (env.has_pressure) { + rsp[i++] = CH_SELF; + rsp[i++] = LPP_BAROMETRIC_PRESSURE; + uint16_t press = (uint16_t)(env.pressure_hpa * 10); + rsp[i++] = (press >> 8) & 0xFF; + rsp[i++] = press & 0xFF; + } + } + } + + // Power monitor telemetry (INA219/INA3221/ina2xx) + if (power_sensors_available()) { + struct power_data pwr; + if (power_sensors_read(&pwr) == 0) { + uint8_t ch = CH_SELF + 1; + for (int j = 0; j < pwr.num_channels; j++) { + if (pwr.channels[j].valid) { + // Voltage: [ch][LPP_VOLTAGE=116][2-byte 0.01V] + rsp[i++] = ch; + rsp[i++] = 116; + uint16_t v = (uint16_t)(pwr.channels[j].voltage_v * 100); + rsp[i++] = (v >> 8) & 0xFF; + rsp[i++] = v & 0xFF; + // Current: [ch][LPP_CURRENT=117][2-byte 0.001A] + rsp[i++] = ch; + rsp[i++] = 117; + uint16_t c = (uint16_t)(pwr.channels[j].current_a * 1000); + rsp[i++] = (c >> 8) & 0xFF; + rsp[i++] = c & 0xFF; + // Power: [ch][LPP_POWER=128][2-byte 1W] + rsp[i++] = ch; + rsp[i++] = 128; + uint16_t p = (uint16_t)(pwr.channels[j].power_w); + rsp[i++] = (p >> 8) & 0xFF; + rsp[i++] = p & 0xFF; + ch++; + } + } + } + } + + writeFrame(rsp, i); + + // Trigger GPS wake so next telemetry request has fresh location + if (gps_is_available() && gps_is_enabled()) { + gps_request_fresh_fix(); + } + } else if (len >= 4 + PUB_KEY_SIZE) { + // Contact telemetry request: [cmd][3 reserved bytes][32-byte pubkey] + ContactInfo *contact = lookupContactByPubKey(&data[4], PUB_KEY_SIZE); + if (contact) { + uint32_t tag, est_timeout; + int result = sendRequest(*contact, REQ_TYPE_GET_TELEMETRY_DATA, tag, est_timeout); + if (result != MSG_SEND_FAILED) { + clearPendingReqs(); + _pending_telemetry = tag; + uint8_t rsp[10]; + rsp[0] = PACKET_SENT; + rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + put_le32(&rsp[2], tag); + put_le32(&rsp[6], est_timeout); + writeFrame(rsp, sizeof(rsp)); + } else { + sendPacketError(ERR_BAD_STATE); + } + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SEND_BINARY_REQ: + // Format: [cmd][32-byte pubkey][req_data...] + if (len >= 1 + PUB_KEY_SIZE + 1) { // Need at least cmd + pubkey + 1 byte req_data + ContactInfo *contact = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + if (contact) { + uint32_t tag, est_timeout; + // req_data starts after pubkey + const uint8_t *req_data = &data[1 + PUB_KEY_SIZE]; + size_t req_len = len - (1 + PUB_KEY_SIZE); + int result = sendRequest(*contact, req_data, req_len, tag, est_timeout); + if (result != MSG_SEND_FAILED) { + clearPendingReqs(); + _pending_req = tag; + uint8_t rsp[10]; + rsp[0] = PACKET_SENT; + rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + put_le32(&rsp[2], tag); + put_le32(&rsp[6], est_timeout); + writeFrame(rsp, sizeof(rsp)); + } else { + sendPacketError(ERR_BAD_STATE); + } + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SEND_PATH_DISCOVERY: + if (len >= 2 + PUB_KEY_SIZE && data[1] == 0) { + // Path Discovery is a special flood + Telemetry request + ContactInfo *contact = lookupContactByPubKey(&data[2], PUB_KEY_SIZE); + if (contact) { + uint32_t tag, est_timeout; + // Build telemetry request with only BASE permissions + uint8_t req_data[9]; + req_data[0] = REQ_TYPE_GET_TELEMETRY_DATA; + req_data[1] = ~(TELEM_PERM_BASE); // inverse permissions mask + memset(&req_data[2], 0, 3); // reserved + getRNG()->random(&req_data[5], 4); // random to make packet-hash unique + + // Temporarily force flood + auto save = contact->out_path_len; + contact->out_path_len = -1; + int result = sendRequest(*contact, req_data, sizeof(req_data), tag, est_timeout); + contact->out_path_len = save; + + if (result != MSG_SEND_FAILED) { + clearPendingReqs(); + _pending_discovery = tag; + uint8_t rsp[10]; + rsp[0] = PACKET_SENT; + rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + put_le32(&rsp[2], tag); + put_le32(&rsp[6], est_timeout); + writeFrame(rsp, sizeof(rsp)); + } else { + sendPacketError(ERR_BAD_STATE); + } + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_GET_ADVERT_PATH: + // Format: [cmd][reserved][7-byte pubkey prefix] + if (len >= 2 + 7) { + const AdvertPath *ap = findAdvertPath(&data[2], 7); // pubkey at offset 2 + if (ap) { + // Response: [code][4-byte timestamp][path_len][path...] + uint8_t rsp[6 + MAX_PATH_SIZE]; + size_t i = 0; + rsp[i++] = PACKET_ADVERT_PATH; + put_le32(&rsp[i], ap->recv_timestamp); i += 4; + rsp[i++] = ap->path_len; + memcpy(&rsp[i], ap->path, ap->path_len); + i += ap->path_len; + writeFrame(rsp, i); + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_GET_CUSTOM_VARS: { + // Return GPS and sensor settings as comma-separated key:value pairs + // Format: [PACKET_CUSTOM_VARS][key1:val1,key2:val2,...] + uint8_t rsp[64]; + char *dp = (char *)&rsp[1]; + rsp[0] = PACKET_CUSTOM_VARS; + + // GPS settings + bool first = true; + if (gps_is_available()) { + dp += snprintf(dp, 20, "gps:%d", gps_is_enabled() ? 1 : 0); + first = false; + } + uint32_t gps_interval = gps_get_poll_interval_sec(); + if (gps_interval > 0) { + if (!first) *dp++ = ','; + dp += snprintf(dp, 20, "gps_interval:%u", gps_interval); + first = false; + } + // Note: Environment sensors are auto-detected, no settings needed + + writeFrame(rsp, (size_t)(dp - (char *)rsp)); + return true; + } + + case CMD_SET_CUSTOM_VAR: + // Format: [cmd][key:value] + if (len >= 4) { + char buf[64]; + size_t copy_len = (len - 1 < sizeof(buf) - 1) ? len - 1 : sizeof(buf) - 1; + memcpy(buf, &data[1], copy_len); + buf[copy_len] = '\0'; + + char *sep = strchr(buf, ':'); + if (sep) { + *sep = '\0'; + char *key = buf; + char *val = sep + 1; + + if (strcmp(key, "gps") == 0) { + bool enable = (val[0] == '1'); + gps_enable(enable); + /* Persist GPS state across reboots */ + prefs.gps_enabled = enable ? 1 : 0; + _store->savePrefs(prefs); + sendPacketOk(); + } else if (strcmp(key, "gps_interval") == 0) { + uint32_t interval = (uint32_t)atoi(val); + if (interval > 0 && interval <= 86400) { + gps_set_poll_interval_sec(interval); + /* Persist GPS interval across reboots */ + prefs.gps_interval = interval; + _store->savePrefs(prefs); + sendPacketOk(); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SET_FLOOD_SCOPE: + /* Flood scope: [cmd][0][16-byte key] or [cmd][0] (null key) */ + if (len >= 2 && data[1] == 0) { + if (len >= 2 + 16) { + memcpy(_send_scope.key, &data[2], sizeof(_send_scope.key)); + } else { + memset(_send_scope.key, 0, sizeof(_send_scope.key)); + } + sendPacketOk(); + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SEND_CONTROL_DATA: + /* Control data: [cmd][flags | 0x80][data...] */ + if (len >= 2 && (data[1] & 0x80) != 0) { + mesh::Packet *pkt = createControlData(&data[1], len - 1); + if (pkt) { + sendZeroHop(pkt); + sendPacketOk(); + } else { + sendPacketError(ERR_TABLE_FULL); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + case CMD_SET_AUTOADD_CONFIG: + if (len >= 2) { + prefs.autoadd_config = data[1]; + LOG_INF("SET_AUTOADD_CONFIG: autoadd_config=0x%02x", prefs.autoadd_config); + _store->savePrefs(prefs); + } + sendPacketOk(); + return true; + + case CMD_GET_AUTOADD_CONFIG: { + uint8_t rsp[2]; + rsp[0] = PACKET_AUTOADD_CONFIG; + rsp[1] = prefs.autoadd_config; + writeFrame(rsp, sizeof(rsp)); + return true; + } + + case CMD_GET_ALLOWED_REPEAT_FREQ: { + uint8_t rsp[1 + sizeof(repeat_freq_ranges)]; + int i = 0; + rsp[i++] = PACKET_ALLOWED_REPEAT_FREQ; + for (size_t k = 0; k < sizeof(repeat_freq_ranges) / sizeof(repeat_freq_ranges[0]); k++) { + put_le32(&rsp[i], repeat_freq_ranges[k].lower_freq); i += 4; + put_le32(&rsp[i], repeat_freq_ranges[k].upper_freq); i += 4; + } + writeFrame(rsp, i); + return true; + } + + case CMD_SEND_ANON_REQ: + if (len >= 1 + PUB_KEY_SIZE + 1) { + ContactInfo *contact = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + if (contact) { + uint32_t tag, est_timeout; + int result = sendAnonReq(*contact, &data[1 + PUB_KEY_SIZE], len - 1 - PUB_KEY_SIZE, tag, est_timeout); + if (result != MSG_SEND_FAILED) { + clearPendingReqs(); + _pending_req = tag; + uint8_t rsp[10]; + rsp[0] = PACKET_SENT; + rsp[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; // is_flood + put_le32(&rsp[2], tag); + put_le32(&rsp[6], est_timeout); + writeFrame(rsp, sizeof(rsp)); + } else { + sendPacketError(ERR_BAD_STATE); + } + } else { + sendPacketError(ERR_NOT_FOUND); + } + } else { + sendPacketError(ERR_ILLEGAL_ARG); + } + return true; + + default: + break; + } + + return false; +} diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h new file mode 100644 index 0000000..b1fbb34 --- /dev/null +++ b/zephcore/app/CompanionMesh.h @@ -0,0 +1,319 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * CompanionMesh - ZephCore Companion device application layer + */ + +#pragma once + +#include +#include +#include +#include + +/* BLE push notification codes - matches Arduino */ +#define PUSH_CODE_ADVERT 0x80 +#define PUSH_CODE_PATH_UPDATED 0x81 +#define PUSH_CODE_SEND_CONFIRMED 0x82 +#define PUSH_CODE_MSG_WAITING 0x83 +#define PUSH_CODE_RAW_DATA 0x84 +#define PUSH_CODE_LOGIN_SUCCESS 0x85 +#define PUSH_CODE_LOGIN_FAIL 0x86 +#define PUSH_CODE_STATUS_RESPONSE 0x87 +#define PUSH_CODE_LOG_RX_DATA 0x88 +#define PUSH_CODE_TRACE_DATA 0x89 +#define PUSH_CODE_NEW_ADVERT 0x8A +#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B +#define PUSH_CODE_BINARY_RESPONSE 0x8C +#define PUSH_CODE_PATH_DISCOVERY_RESP 0x8D +#define PUSH_CODE_CONTROL_DATA 0x8E +#define PUSH_CODE_CONTACT_DELETED 0x8F +#define PUSH_CODE_CONTACTS_FULL 0x90 + +/* Auto-add config bitmask */ +#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) +#define AUTO_ADD_CHAT (1 << 1) +#define AUTO_ADD_REPEATER (1 << 2) +#define AUTO_ADD_ROOM_SERVER (1 << 3) +#define AUTO_ADD_SENSOR (1 << 4) + +/* Maximum BLE frame size — must match NUS MTU negotiated size. + * All protocol response buffers use this. */ +#define MAX_FRAME_SIZE 172 + +/* Contact frame size: 1 header + 32 pubkey + 1 type + 1 flags + 1 path_len + * + 64 path + 32 name + 4 timestamp + 4 lat + 4 lon + 4 lastmod = 148 */ +#define CONTACT_FRAME_SIZE 148 + +/* Offline message queue size */ +#ifdef CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE +#define OFFLINE_QUEUE_SIZE CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE +#else +#define OFFLINE_QUEUE_SIZE 16 +#endif + +/* ACK table size */ +#ifdef CONFIG_ZEPHCORE_ACK_TABLE_SIZE +#define ACK_TABLE_SIZE CONFIG_ZEPHCORE_ACK_TABLE_SIZE +#else +#define ACK_TABLE_SIZE 8 +#endif + +/* Advert path table size */ +#ifdef CONFIG_ZEPHCORE_ADVERT_PATH_TABLE_SIZE +#define ADVERT_PATH_TABLE_SIZE CONFIG_ZEPHCORE_ADVERT_PATH_TABLE_SIZE +#else +#define ADVERT_PATH_TABLE_SIZE 16 +#endif + +/* Structure for tracking recently heard adverts */ +struct AdvertPath { + uint8_t pubkey_prefix[7]; + uint8_t path_len; + char name[32]; + uint32_t recv_timestamp; + uint8_t path[MAX_PATH_SIZE]; +}; + +/* Callback type for BLE push notifications */ +typedef void (*PushCallback)(uint8_t code, const uint8_t *data, size_t len); + +/* Callback type for write frame */ +typedef size_t (*WriteFrameCallback)(const uint8_t *data, size_t len); + +/* Callback for getting battery millivolts */ +typedef uint16_t (*GetBatteryCallback)(void); + +/* Callback for radio reconfigure */ +typedef void (*RadioReconfigureCallback)(void); + +/* Callback for BLE PIN change */ +typedef void (*PinChangeCallback)(uint32_t new_pin); + +/** + * CompanionMesh: Application layer for ZephCore Companion device + * + * Extends BaseChatMesh with: + * - BLE protocol frame handling + * - Offline message queue + * - Push notifications for incoming messages/adverts + * - ACK tracking for sent messages + */ +class CompanionMesh : public BaseChatMesh, public DataStoreHost { +public: + CompanionMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng, + mesh::RTCClock &rtc, mesh::PacketManager &mgr, mesh::MeshTables &tables, + ZephyrDataStore &store); + + void begin(); + void loop(); + + /** + * Handle a protocol frame from BLE. + * Returns true if frame was handled. + */ + bool handleProtocolFrame(const uint8_t *data, size_t len); + + /** + * Set callback for BLE push notifications. + */ + void setPushCallback(PushCallback cb) { _push_cb = cb; } + + /** + * Set callback for writing response frames to BLE. + */ + void setWriteFrameCallback(WriteFrameCallback cb) { _write_cb = cb; } + + /** + * Set callback for getting battery voltage. + */ + void setBatteryCallback(GetBatteryCallback cb) { _batt_cb = cb; } + + /** + * Set callback for radio reconfigure. + */ + void setRadioReconfigureCallback(RadioReconfigureCallback cb) { _radio_reconfig_cb = cb; } + + /** + * Set callback for BLE PIN change. + */ + void setPinChangeCallback(PinChangeCallback cb) { _pin_change_cb = cb; } + + /** + * Continue contact iteration (call each main loop iteration). + * Returns true if contacts are still being sent. + */ + bool continueContactIteration(); + + /** + * Reset contact iterator (call when new command received). + */ + void resetContactIterator(); + + /** + * Get BLE device name for advertising. + */ + const char *getDeviceName() const { return prefs.node_name[0] ? prefs.node_name : nullptr; } + + /** + * Get recently heard advert paths. + */ + int getRecentlyHeard(AdvertPath dest[], int max_num); + + /** + * Find advert path by pubkey prefix. + */ + const AdvertPath *findAdvertPath(const uint8_t *pubkey_prefix, int prefix_len); + + /* DataStoreHost interface */ + bool onContactLoaded(const ContactInfo &c) override; + bool getContactForSave(uint32_t idx, ContactInfo &c) override; + bool onChannelLoaded(uint8_t idx, const ChannelDetails &ch) override; + bool getChannelForSave(uint8_t idx, ChannelDetails &ch) override; + + /* Prefs (includes node_lat/lon) */ + NodePrefs prefs; + +protected: + /* BaseChatMesh virtual implementations */ + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t *path) override; + ContactInfo *processAck(const uint8_t *data) override; + void onContactPathUpdated(const ContactInfo &contact) override; + void onMessageRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) override; + void onCommandDataRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) override; + void onSignedMessageRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override; + uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override; + uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override; + void onSendTimeout() override; + void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, const char *text) override; + uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) override; + void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) override; + + /* Raw packet logging for app RX log */ + void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; + + /* Trace path response */ + void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) override; + + /* Control data response (repeater discovery, etc) */ + void onControlDataRecv(mesh::Packet *packet) override; + + /* Raw data response (custom packets) */ + void onRawDataRecv(mesh::Packet *packet) override; + + /* Packet forwarding (client repeat / offgrid mode) */ + bool allowPacketForward(const mesh::Packet *packet) override; + + /* Path discovery - intercept path data before base class strips it */ + bool onContactPathRecv(ContactInfo &from, uint8_t *in_path, uint8_t in_path_len, + uint8_t *out_path, uint8_t out_path_len, uint8_t extra_type, + uint8_t *extra, uint8_t extra_len) override; + + /* Flood scope - scoped sending for region filtering */ + void sendFloodScoped(const ContactInfo &recipient, mesh::Packet *pkt, uint32_t delay_millis = 0) override; + void sendFloodScoped(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t delay_millis = 0) override; + + /* Dispatcher tuning (uses prefs) */ + uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override; + uint8_t getDutyCyclePercent() const override; + uint8_t getExtraAckTransmitCount() const override; + + /* Auto-add filtering overrides */ + bool isAutoAddEnabled() const override; + bool shouldAutoAddContactType(uint8_t type) const override; + bool shouldOverwriteWhenFull() const override; + void onContactsFull() override; + void onContactOverwrite(const uint8_t *pub_key) override; + + /* Storage overrides */ + int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override; + bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) override; + +private: + ZephyrDataStore *_store; + PushCallback _push_cb; + WriteFrameCallback _write_cb; + GetBatteryCallback _batt_cb; + RadioReconfigureCallback _radio_reconfig_cb; + PinChangeCallback _pin_change_cb; + + /* Contact iteration state */ + bool _contact_iter_active; + int _contact_iter_idx; + uint32_t _contact_iter_lastmod; + uint32_t _contact_iter_since; /* Filter: only send contacts with lastmod > this */ + + /* Offline message queue */ + struct QueuedFrame { + uint8_t len; + uint8_t buf[172]; + }; + QueuedFrame _offline_queue[OFFLINE_QUEUE_SIZE]; + int _offline_queue_head; + int _offline_queue_tail; + int _offline_queue_count; + + /* ACK tracking table */ + struct AckEntry { + uint32_t expected_ack; + uint32_t sent_time; + int contact_idx; + bool active; + }; + AckEntry _ack_table[ACK_TABLE_SIZE]; + int _ack_next_overwrite; + + /* Advert path table for tracking recently heard nodes */ + AdvertPath _advert_paths[ADVERT_PATH_TABLE_SIZE]; + int _next_advert_path_idx; + + /* Signing state */ + uint8_t *_sign_data; + uint32_t _sign_data_len; + uint32_t _sign_data_capacity; + + /* Pending request tracking (for response matching) */ + uint32_t _pending_login; + uint32_t _pending_status; + uint32_t _pending_telemetry; + uint32_t _pending_discovery; + uint32_t _pending_req; + + /* Lazy contacts/channels write - batches rapid updates */ + int64_t _dirty_contacts_expiry; + int64_t _dirty_channels_expiry; + static constexpr int64_t LAZY_WRITE_DELAY_MS = 5000; /* 5 seconds, matches Arduino */ + + void markContactsDirty(); + void markChannelsDirty(); + void flushDirtyContacts(); + void flushDirtyChannels(); + + /* Protocol version negotiation */ + uint8_t _app_target_ver; + + /* Flood scope for transport filtering (all zeros = disabled) */ + TransportKey _send_scope; + + void clearPendingReqs() { + _pending_login = _pending_status = _pending_telemetry = _pending_discovery = _pending_req = 0; + } + + void writeFrame(const uint8_t *data, size_t len); + void sendPacketOk(); + void sendPacketError(uint8_t code); + void sendPush(uint8_t code, const uint8_t *data = nullptr, size_t len = 0); + + /** Serialize a ContactInfo into buf. If header != 0, prepend it. + * Returns total bytes written. buf must be >= CONTACT_FRAME_SIZE. */ + static size_t serializeContact(uint8_t *buf, const ContactInfo &c, uint8_t header = 0); + + void queueOfflineMessage(const uint8_t *data, size_t len); + bool dequeueOfflineMessage(uint8_t *dest, size_t &len); + + void queueContactMessage(const ContactInfo &contact, mesh::Packet *pkt, + uint8_t txt_type, uint32_t sender_timestamp, const uint8_t *extra, int extra_len, const char *text); + + void addPendingAck(uint32_t expected, int contact_idx); + int findAndRemoveAck(uint32_t ack, uint32_t *out_sent_time = nullptr); +}; diff --git a/zephcore/app/RepeaterDataStore.cpp b/zephcore/app/RepeaterDataStore.cpp new file mode 100644 index 0000000..748b42c --- /dev/null +++ b/zephcore/app/RepeaterDataStore.cpp @@ -0,0 +1,263 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * RepeaterDataStore - Filesystem storage for repeater + */ + +#include "RepeaterDataStore.h" +#include +#include +#include +#include + +LOG_MODULE_REGISTER(zephcore_repeater_store, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); + +/* File paths for repeater data */ +#define REPEATER_DIR "/lfs/repeater" +#define IDENTITY_FILE "/lfs/repeater/_main.id" +#define PREFS_FILE "/lfs/repeater/prefs" + +RepeaterDataStore::RepeaterDataStore() : _initialized(false) { +} + +bool RepeaterDataStore::begin() { + if (_initialized) return true; + + /* Create repeater directory if it doesn't exist */ + struct fs_dirent entry; + int ret = fs_stat(REPEATER_DIR, &entry); + if (ret < 0) { + ret = fs_mkdir(REPEATER_DIR); + if (ret < 0 && ret != -EEXIST) { + LOG_ERR("Failed to create %s: %d", REPEATER_DIR, ret); + return false; + } + LOG_INF("Created %s directory", REPEATER_DIR); + } + + _initialized = true; + LOG_INF("RepeaterDataStore initialized"); + return true; +} + +bool RepeaterDataStore::loadIdentity(mesh::LocalIdentity& id) { + struct fs_file_t file; + fs_file_t_init(&file); + + int ret = fs_open(&file, IDENTITY_FILE, FS_O_READ); + if (ret < 0) { + LOG_WRN("No identity file at %s", IDENTITY_FILE); + return false; // Caller must generate new identity + } + + uint8_t buf[PRV_KEY_SIZE + PUB_KEY_SIZE]; + ssize_t n = fs_read(&file, buf, sizeof(buf)); + fs_close(&file); + + LOG_INF("loadIdentity: read %d bytes from %s", (int)n, IDENTITY_FILE); + + if (n >= PRV_KEY_SIZE) { + LOG_INF("loadIdentity: calling readFrom (will derive pubkey if n=%d)", (int)n); + if (id.readFrom(buf, n)) { + LOG_INF("Loaded identity from %s", IDENTITY_FILE); + return true; + } + LOG_ERR("loadIdentity: readFrom failed"); + } + + LOG_ERR("Identity file corrupt"); + return false; // Caller must generate new identity +} + +bool RepeaterDataStore::saveIdentity(const mesh::LocalIdentity& id) { + /* Ensure directory exists */ + if (!_initialized) begin(); + + /* Remove old file first */ + fs_unlink(IDENTITY_FILE); + + struct fs_file_t file; + fs_file_t_init(&file); + + int ret = fs_open(&file, IDENTITY_FILE, FS_O_CREATE | FS_O_WRITE); + if (ret < 0) { + LOG_ERR("Failed to open %s for write: %d", IDENTITY_FILE, ret); + return false; + } + + uint8_t buf[PRV_KEY_SIZE]; + int len = id.writeTo(buf, sizeof(buf)); + ssize_t n = fs_write(&file, buf, len); + fs_close(&file); + + if (n == len) { + LOG_INF("Saved identity to %s", IDENTITY_FILE); + return true; + } + + LOG_ERR("Failed to write identity: wrote %d of %d", (int)n, len); + return false; +} + +bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) { + struct fs_file_t file; + fs_file_t_init(&file); + + int ret = fs_open(&file, PREFS_FILE, FS_O_READ); + if (ret < 0) { + LOG_DBG("No prefs file at %s, using defaults", PREFS_FILE); + initNodePrefs(&prefs); + strcpy(prefs.node_name, "Repeater"); + return true; + } + + /* Get file size first */ + struct fs_dirent entry; + ret = fs_stat(PREFS_FILE, &entry); + LOG_INF("loadPrefs: file size = %d bytes", ret < 0 ? 0 : (int)entry.size); + + uint8_t pad[8]; + + /* Read prefs in same format as Arduino CommonCLI for compatibility */ + fs_read(&file, &prefs.airtime_factor, sizeof(prefs.airtime_factor)); + fs_read(&file, &prefs.node_name, sizeof(prefs.node_name)); + fs_read(&file, pad, 4); + fs_read(&file, &prefs.node_lat, sizeof(prefs.node_lat)); + fs_read(&file, &prefs.node_lon, sizeof(prefs.node_lon)); + fs_read(&file, &prefs.password, sizeof(prefs.password)); + fs_read(&file, &prefs.freq, sizeof(prefs.freq)); + fs_read(&file, &prefs.tx_power_dbm, sizeof(prefs.tx_power_dbm)); + fs_read(&file, &prefs.disable_fwd, sizeof(prefs.disable_fwd)); + fs_read(&file, &prefs.advert_interval, sizeof(prefs.advert_interval)); + fs_read(&file, pad, 1); + fs_read(&file, &prefs.rx_delay_base, sizeof(prefs.rx_delay_base)); + fs_read(&file, &prefs.tx_delay_factor, sizeof(prefs.tx_delay_factor)); + fs_read(&file, &prefs.guest_password, sizeof(prefs.guest_password)); + fs_read(&file, &prefs.direct_tx_delay_factor, sizeof(prefs.direct_tx_delay_factor)); + fs_read(&file, pad, 4); + fs_read(&file, &prefs.sf, sizeof(prefs.sf)); + fs_read(&file, &prefs.cr, sizeof(prefs.cr)); + fs_read(&file, &prefs.allow_read_only, sizeof(prefs.allow_read_only)); + fs_read(&file, &prefs.multi_acks, sizeof(prefs.multi_acks)); + fs_read(&file, &prefs.bw, sizeof(prefs.bw)); + fs_read(&file, &prefs.agc_reset_interval, sizeof(prefs.agc_reset_interval)); + fs_read(&file, pad, 3); + fs_read(&file, &prefs.flood_max, sizeof(prefs.flood_max)); + fs_read(&file, &prefs.flood_advert_interval, sizeof(prefs.flood_advert_interval)); + fs_read(&file, &prefs.interference_threshold, sizeof(prefs.interference_threshold)); + fs_read(&file, pad, 25); // skip bridge settings + fs_read(&file, &prefs.powersaving_enabled, sizeof(prefs.powersaving_enabled)); + fs_read(&file, pad, 3); + fs_read(&file, &prefs.gps_enabled, sizeof(prefs.gps_enabled)); + fs_read(&file, &prefs.gps_interval, sizeof(prefs.gps_interval)); + fs_read(&file, &prefs.advert_loc_policy, sizeof(prefs.advert_loc_policy)); + fs_read(&file, &prefs.discovery_mod_timestamp, sizeof(prefs.discovery_mod_timestamp)); + fs_read(&file, &prefs.adc_multiplier, sizeof(prefs.adc_multiplier)); + fs_read(&file, prefs.owner_info, sizeof(prefs.owner_info)); + + fs_close(&file); + LOG_INF("Loaded prefs from %s", PREFS_FILE); + LOG_INF(" name='%s' freq=%.3f sf=%u bw=%.1f tx_pwr=%d", + prefs.node_name, (double)prefs.freq, prefs.sf, (double)prefs.bw, prefs.tx_power_dbm); + + /* Validate radio params - use defaults if garbage */ + if (prefs.freq < 300.0f || prefs.freq > 1000.0f || + prefs.sf < 5 || prefs.sf > 12 || + prefs.bw < 7.0f || prefs.bw > 500.0f) { + LOG_WRN("Invalid radio params in prefs, using defaults: freq=%.3f sf=%u bw=%.1f", + (double)prefs.freq, prefs.sf, (double)prefs.bw); + prefs.freq = 869.618f; + prefs.bw = 62.5f; + prefs.sf = 8; + prefs.cr = 8; + prefs.tx_power_dbm = 22; + } + return true; +} + +bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) { + /* Ensure directory exists */ + if (!_initialized) begin(); + + /* Remove old file first */ + fs_unlink(PREFS_FILE); + + struct fs_file_t file; + fs_file_t_init(&file); + + int ret = fs_open(&file, PREFS_FILE, FS_O_CREATE | FS_O_WRITE); + if (ret < 0) { + LOG_ERR("Failed to open %s for write: %d", PREFS_FILE, ret); + return false; + } + + uint8_t pad[25]; + memset(pad, 0, sizeof(pad)); + + /* Write prefs in same format as Arduino CommonCLI for compatibility */ + fs_write(&file, &prefs.airtime_factor, sizeof(prefs.airtime_factor)); + fs_write(&file, &prefs.node_name, sizeof(prefs.node_name)); + fs_write(&file, pad, 4); + fs_write(&file, &prefs.node_lat, sizeof(prefs.node_lat)); + fs_write(&file, &prefs.node_lon, sizeof(prefs.node_lon)); + fs_write(&file, &prefs.password, sizeof(prefs.password)); + fs_write(&file, &prefs.freq, sizeof(prefs.freq)); + fs_write(&file, &prefs.tx_power_dbm, sizeof(prefs.tx_power_dbm)); + fs_write(&file, &prefs.disable_fwd, sizeof(prefs.disable_fwd)); + fs_write(&file, &prefs.advert_interval, sizeof(prefs.advert_interval)); + fs_write(&file, pad, 1); + fs_write(&file, &prefs.rx_delay_base, sizeof(prefs.rx_delay_base)); + fs_write(&file, &prefs.tx_delay_factor, sizeof(prefs.tx_delay_factor)); + fs_write(&file, &prefs.guest_password, sizeof(prefs.guest_password)); + fs_write(&file, &prefs.direct_tx_delay_factor, sizeof(prefs.direct_tx_delay_factor)); + fs_write(&file, pad, 4); + fs_write(&file, &prefs.sf, sizeof(prefs.sf)); + fs_write(&file, &prefs.cr, sizeof(prefs.cr)); + fs_write(&file, &prefs.allow_read_only, sizeof(prefs.allow_read_only)); + fs_write(&file, &prefs.multi_acks, sizeof(prefs.multi_acks)); + fs_write(&file, &prefs.bw, sizeof(prefs.bw)); + fs_write(&file, &prefs.agc_reset_interval, sizeof(prefs.agc_reset_interval)); + fs_write(&file, pad, 3); + fs_write(&file, &prefs.flood_max, sizeof(prefs.flood_max)); + fs_write(&file, &prefs.flood_advert_interval, sizeof(prefs.flood_advert_interval)); + fs_write(&file, &prefs.interference_threshold, sizeof(prefs.interference_threshold)); + fs_write(&file, pad, 25); // skip bridge settings + fs_write(&file, &prefs.powersaving_enabled, sizeof(prefs.powersaving_enabled)); + fs_write(&file, pad, 3); + fs_write(&file, &prefs.gps_enabled, sizeof(prefs.gps_enabled)); + fs_write(&file, &prefs.gps_interval, sizeof(prefs.gps_interval)); + fs_write(&file, &prefs.advert_loc_policy, sizeof(prefs.advert_loc_policy)); + fs_write(&file, &prefs.discovery_mod_timestamp, sizeof(prefs.discovery_mod_timestamp)); + fs_write(&file, &prefs.adc_multiplier, sizeof(prefs.adc_multiplier)); + fs_write(&file, prefs.owner_info, sizeof(prefs.owner_info)); + + fs_close(&file); + LOG_INF("Saved prefs to %s", PREFS_FILE); + return true; +} + +bool RepeaterDataStore::formatFileSystem() { + LOG_WRN("Factory reset: erasing repeater data..."); + + /* Delete all files in /lfs/repeater/ */ + struct fs_dir_t dir; + fs_dir_t_init(&dir); + + int ret = fs_opendir(&dir, REPEATER_DIR); + if (ret < 0) { + LOG_WRN("No repeater directory to erase"); + return true; + } + + struct fs_dirent entry; + char path[280]; /* REPEATER_DIR (14) + "/" (1) + MAX_FILE_NAME (255) + null */ + + while (fs_readdir(&dir, &entry) == 0 && entry.name[0] != '\0') { + snprintf(path, sizeof(path), "%s/%s", REPEATER_DIR, entry.name); + LOG_INF("Deleting %s", path); + fs_unlink(path); + } + fs_closedir(&dir); + + LOG_INF("Repeater data erased"); + return true; +} diff --git a/zephcore/app/RepeaterDataStore.h b/zephcore/app/RepeaterDataStore.h new file mode 100644 index 0000000..6747c21 --- /dev/null +++ b/zephcore/app/RepeaterDataStore.h @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * RepeaterDataStore - Filesystem storage for repeater + * + * Uses /lfs/repeater/ prefix to keep data separate from companion. + * This allows flashing back and forth between roles without corruption. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class RepeaterDataStore { +public: + RepeaterDataStore(); + + /* Initialize filesystem and repeater directory */ + bool begin(); + + /* Identity management */ + bool loadIdentity(mesh::LocalIdentity& id); + bool saveIdentity(const mesh::LocalIdentity& id); + + /* Prefs management */ + bool loadPrefs(NodePrefs& prefs); + bool savePrefs(const NodePrefs& prefs); + + /* ACL management - paths passed to ClientACL */ + const char* getAclPath() const { return "/lfs/repeater/acl"; } + + /* Region management - paths passed to RegionMap */ + const char* getRegionsPath() const { return "/lfs/repeater/regions2"; } + + /* Factory reset - erase all repeater data */ + bool formatFileSystem(); + + /* Get base path for repeater storage */ + static const char* getBasePath() { return "/lfs/repeater"; } + +private: + bool _initialized; +}; diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp new file mode 100644 index 0000000..8cc9d66 --- /dev/null +++ b/zephcore/app/RepeaterMesh.cpp @@ -0,0 +1,1168 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * RepeaterMesh - LoRa mesh repeater implementation + */ + +#include "RepeaterMesh.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Helper to get radio driver for stats — uses LoRaRadioBase (works for SX126x and LR1110) */ +static inline mesh::LoRaRadioBase& getRadioDriver(mesh::Radio* radio) { + return *static_cast(radio); +} + +/* Simple sort helper since is not available in Zephyr minimal C++ */ +template +static void simple_sort(T* arr, int count, Comparator cmp) { + for (int i = 0; i < count - 1; i++) { + for (int j = i + 1; j < count; j++) { + if (cmp(arr[j], arr[i])) { + T temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + } + } +} + +LOG_MODULE_REGISTER(zephcore_repeater, LOG_LEVEL_INF); + +/* Protocol constants */ +#define FIRMWARE_VER_LEVEL 2 + +#define REQ_TYPE_GET_STATUS 0x01 +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +#define REQ_TYPE_GET_ACCESS_LIST 0x05 +#define REQ_TYPE_GET_NEIGHBOURS 0x06 +#define REQ_TYPE_GET_OWNER_INFO 0x07 + +#define RESP_SERVER_LOGIN_OK 0 + +#define ANON_REQ_TYPE_REGIONS 0x01 +#define ANON_REQ_TYPE_OWNER 0x02 +#define ANON_REQ_TYPE_BASIC 0x03 + +#define CLI_REPLY_DELAY_MILLIS 600 +#define LAZY_CONTACTS_WRITE_DELAY 5000 +#define SERVER_RESPONSE_DELAY 300 +#define TXT_ACK_DELAY 200 + +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + +/* Helper: futureMillis */ +static inline unsigned long futureMillis(uint32_t delta_ms) { + return k_uptime_get() + delta_ms; +} + +static inline bool millisHasNowPassed(unsigned long target) { + return (int64_t)k_uptime_get() >= (int64_t)target; +} + +static void radio_set_tx_power(uint8_t power_dbm) { + /* TX power is configured as part of lora_config() in radio_set_params + * The Zephyr LoRa driver doesn't have a separate lora_set_tx_power API. + * Instead, we log that TX power setting is requested. The actual power + * is set in the board defconfig via CONFIG_LORA_TX_POWER. */ + LOG_INF("TX power %d dBm requested (configured via board defconfig)", power_dbm); +} + +void RepeaterMesh::putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr) { +#if MAX_NEIGHBOURS > 0 + uint32_t oldest_timestamp = 0xFFFFFFFF; + NeighbourInfo* neighbour = &neighbours[0]; + + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (id.matches(neighbours[i].id)) { + neighbour = &neighbours[i]; + break; + } + if (neighbours[i].heard_timestamp < oldest_timestamp) { + neighbour = &neighbours[i]; + oldest_timestamp = neighbour->heard_timestamp; + } + } + + neighbour->id = id; + neighbour->advert_timestamp = timestamp; + neighbour->heard_timestamp = getRTCClock()->getCurrentTime(); + neighbour->snr = (int8_t)(snr * 4); +#endif +} + +uint8_t RepeaterMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) { + ClientInfo* client = nullptr; + + if (data[0] == 0) { + client = acl.getClient(sender.pub_key, PUB_KEY_SIZE); + } + + if (client == nullptr) { + uint8_t perms; + if (strcmp((char*)data, _prefs.password) == 0) { + perms = PERM_ACL_ADMIN; + } else if (strcmp((char*)data, _prefs.guest_password) == 0) { + perms = PERM_ACL_GUEST; + } else { + LOG_DBG("Invalid password"); + return 0; + } + + client = acl.putClient(sender, 0); + if (sender_timestamp <= client->last_timestamp) { + LOG_WRN("Possible login replay attack!"); + return 0; + } + + LOG_INF("Login success"); + client->last_timestamp = sender_timestamp; + client->last_activity = getRTCClock()->getCurrentTime(); + client->permissions &= ~0x03; + client->permissions |= perms; + memcpy(client->shared_secret, secret, PUB_KEY_SIZE); + + if (perms != PERM_ACL_GUEST) { + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + } + } + + if (is_flood) { + client->out_path_len = -1; + } + + uint32_t now = getRTCClock()->getCurrentTimeUnique(); + memcpy(reply_data, &now, 4); + reply_data[4] = RESP_SERVER_LOGIN_OK; + reply_data[5] = 0; + reply_data[6] = client->isAdmin() ? 1 : 0; + reply_data[7] = client->permissions; + getRNG()->random(&reply_data[8], 4); + reply_data[12] = FIRMWARE_VER_LEVEL; + + return 13; +} + +uint8_t RepeaterMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { + if (anon_limiter.allow(getRTCClock()->getCurrentTime())) { + reply_path_len = *data++ & 0x3F; + memcpy(reply_path, data, reply_path_len); + + memcpy(reply_data, &sender_timestamp, 4); + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply_data[4], &now, 4); + + return 8 + region_map.exportNamesTo((char*)&reply_data[8], sizeof(reply_data) - 12, REGION_DENY_FLOOD); + } + return 0; +} + +uint8_t RepeaterMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { + if (anon_limiter.allow(getRTCClock()->getCurrentTime())) { + reply_path_len = *data++ & 0x3F; + memcpy(reply_path, data, reply_path_len); + + memcpy(reply_data, &sender_timestamp, 4); + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply_data[4], &now, 4); + sprintf((char*)&reply_data[8], "%s\n%s", _prefs.node_name, _prefs.owner_info); + + return 8 + strlen((char*)&reply_data[8]); + } + return 0; +} + +uint8_t RepeaterMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { + if (anon_limiter.allow(getRTCClock()->getCurrentTime())) { + reply_path_len = *data++ & 0x3F; + memcpy(reply_path, data, reply_path_len); + + memcpy(reply_data, &sender_timestamp, 4); + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply_data[4], &now, 4); + reply_data[8] = 0; // features + if (_prefs.disable_fwd) { + reply_data[8] |= 0x80; + } + return 9; + } + return 0; +} + +int RepeaterMesh::handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len) { + memcpy(reply_data, &sender_timestamp, 4); + + if (payload[0] == REQ_TYPE_GET_STATUS) { + auto& radio_driver = getRadioDriver(_radio); + RepeaterStats stats; + stats.batt_milli_volts = _board.getBattMilliVolts(); + stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); + stats.noise_floor = (int16_t)_radio->getNoiseFloor(); + stats.last_rssi = (int16_t)radio_driver.getLastRSSI(); + stats.n_packets_recv = radio_driver.getPacketsRecv(); + stats.n_packets_sent = radio_driver.getPacketsSent(); + stats.total_air_time_secs = getTotalAirTime() / 1000; + stats.total_up_time_secs = uptime_millis / 1000; + stats.n_sent_flood = getNumSentFlood(); + stats.n_sent_direct = getNumSentDirect(); + stats.n_recv_flood = getNumRecvFlood(); + stats.n_recv_direct = getNumRecvDirect(); + stats.err_events = _err_flags; + stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); + /* Note: dup counters not implemented in Zephyr SimpleMeshTables */ + stats.n_direct_dups = 0; + stats.n_flood_dups = 0; + stats.total_rx_air_time_secs = getReceiveAirTime() / 1000; + stats.n_recv_errors = radio_driver.getPacketsRecvErrors(); + memcpy(&reply_data[4], &stats, sizeof(stats)); + return 4 + sizeof(stats); + } + + if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) { + /* CayenneLPP telemetry response using SimpleLPP encoder */ + SimpleLPP lpp(&reply_data[4], sizeof(reply_data) - 4); + + /* Battery voltage - channel 0 = self */ + uint16_t batt_mv = _board.getBattMilliVolts(); + lpp.addVoltage(0, batt_mv / 1000.0f); + + /* Environment sensors — prefer external, fallback to MCU die temp */ + struct env_data env; + if (env_sensors_read(&env) == 0) { + if (env.has_temperature) { + lpp.addTemperature(0, env.temperature_c); + } else if (env.has_mcu_temperature) { + lpp.addTemperature(0, env.mcu_temperature_c); + } else { + /* Last resort: MCU temp from board API */ + float mcu_temp = _board.getMCUTemperature(); + if (!isnan(mcu_temp)) { + lpp.addTemperature(0, mcu_temp); + } + } + if (env.has_humidity) { + lpp.addRelativeHumidity(0, env.humidity_pct); + } + if (env.has_pressure) { + lpp.addBarometricPressure(0, env.pressure_hpa); + } + } else { + /* No env sensors at all — try MCU temp directly */ + float mcu_temp = _board.getMCUTemperature(); + if (!isnan(mcu_temp)) { + lpp.addTemperature(0, mcu_temp); + } + } + + /* Power monitors (INA219/INA3221/ina2xx) */ + if (power_sensors_available()) { + struct power_data pwr; + if (power_sensors_read(&pwr) == 0) { + uint8_t ch = 1; + for (int j = 0; j < pwr.num_channels; j++) { + if (pwr.channels[j].valid) { + lpp.addVoltage(ch, pwr.channels[j].voltage_v); + lpp.addCurrent(ch, pwr.channels[j].current_a); + lpp.addPower(ch, pwr.channels[j].power_w); + ch++; + } + } + } + } + + /* TODO: Add GPS location here via sensor manager */ + + return 4 + lpp.getSize(); + } + + if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) { + uint8_t res1 = payload[1]; + uint8_t res2 = payload[2]; + if (res1 == 0 && res2 == 0) { + uint8_t ofs = 4; + for (int i = 0; i < acl.getNumClients() && (size_t)(ofs + 7) <= sizeof(reply_data) - 4; i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0) continue; + memcpy(&reply_data[ofs], c->id.pub_key, 6); + ofs += 6; + reply_data[ofs++] = c->permissions; + } + return ofs; + } + } + + if (payload[0] == REQ_TYPE_GET_NEIGHBOURS) { +#if MAX_NEIGHBOURS > 0 + uint8_t request_version = payload[1]; + if (request_version == 0) { + int reply_offset = 4; + uint8_t count = payload[2]; + uint16_t offset; + memcpy(&offset, &payload[3], 2); + uint8_t order_by = payload[5]; + uint8_t pubkey_prefix_length = payload[6]; + + if (pubkey_prefix_length > PUB_KEY_SIZE) { + pubkey_prefix_length = PUB_KEY_SIZE; + } + + // Create sorted copy + int16_t neighbours_count = 0; + NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS]; + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (neighbours[i].heard_timestamp > 0) { + sorted_neighbours[neighbours_count++] = &neighbours[i]; + } + } + + // Sort + if (order_by == 0) { + simple_sort(sorted_neighbours, (int)neighbours_count, + [](const NeighbourInfo* a, const NeighbourInfo* b) { + return a->heard_timestamp > b->heard_timestamp; + }); + } else if (order_by == 1) { + simple_sort(sorted_neighbours, (int)neighbours_count, + [](const NeighbourInfo* a, const NeighbourInfo* b) { + return a->heard_timestamp < b->heard_timestamp; + }); + } else if (order_by == 2) { + simple_sort(sorted_neighbours, (int)neighbours_count, + [](const NeighbourInfo* a, const NeighbourInfo* b) { + return a->snr > b->snr; + }); + } else if (order_by == 3) { + simple_sort(sorted_neighbours, (int)neighbours_count, + [](const NeighbourInfo* a, const NeighbourInfo* b) { + return a->snr < b->snr; + }); + } + + // Build results + int results_count = 0; + int results_offset = 0; + uint8_t results_buffer[130]; + for (int index = 0; index < count && index + offset < neighbours_count; index++) { + int entry_size = pubkey_prefix_length + 4 + 1; + if (results_offset + entry_size > (int)sizeof(results_buffer)) break; + + auto neighbour = sorted_neighbours[index + offset]; + uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; + memcpy(&results_buffer[results_offset], neighbour->id.pub_key, pubkey_prefix_length); + results_offset += pubkey_prefix_length; + memcpy(&results_buffer[results_offset], &heard_seconds_ago, 4); + results_offset += 4; + memcpy(&results_buffer[results_offset], &neighbour->snr, 1); + results_offset += 1; + results_count++; + } + + memcpy(&reply_data[reply_offset], &neighbours_count, 2); + reply_offset += 2; + memcpy(&reply_data[reply_offset], &results_count, 2); + reply_offset += 2; + memcpy(&reply_data[reply_offset], results_buffer, results_offset); + reply_offset += results_offset; + + return reply_offset; + } +#endif + } + + if (payload[0] == REQ_TYPE_GET_OWNER_INFO) { + sprintf((char*)&reply_data[4], "%s\n%s\n%s", FIRMWARE_VERSION, _prefs.node_name, _prefs.owner_info); + return 4 + strlen((char*)&reply_data[4]); + } + + return 0; +} + +mesh::Packet* RepeaterMesh::createSelfAdvert() { + uint8_t app_data[MAX_ADVERT_DATA_SIZE]; + uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_REPEATER, app_data); + return createAdvert(self_id, app_data, app_data_len); +} + +bool RepeaterMesh::allowPacketForward(const mesh::Packet* packet) { + if (_prefs.disable_fwd) { + LOG_INF("allowPacketForward: BLOCKED - disable_fwd=1"); + return false; + } + if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) { + LOG_INF("allowPacketForward: BLOCKED - path_len=%d >= flood_max=%d", + packet->path_len, _prefs.flood_max); + return false; + } + if (packet->isRouteFlood() && recv_pkt_region == nullptr) { + LOG_INF("allowPacketForward: BLOCKED - recv_pkt_region=NULL (route_type=0x%02x)", + packet->getRouteType()); + return false; + } + LOG_INF("allowPacketForward: ALLOWED (flood=%d path_len=%d)", + packet->isRouteFlood() ? 1 : 0, packet->path_len); + return true; +} + +const char* RepeaterMesh::getLogDateTime() { + static char tmp[48]; + uint32_t now = getRTCClock()->getCurrentTime(); + /* Match Arduino format: "HH:MM:SS - D/M/YYYY U" */ + time_t t = (time_t)now; + struct tm* tm = gmtime(&t); + if (tm) { + snprintf(tmp, sizeof(tmp), "%02d:%02d:%02d - %d/%d/%d U", + tm->tm_hour, tm->tm_min, tm->tm_sec, + tm->tm_mday, tm->tm_mon + 1, tm->tm_year + 1900); + } else { + snprintf(tmp, sizeof(tmp), "%u", now); + } + return tmp; +} + +void RepeaterMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { +#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING) + /* Arduino-compatible RAW packet hex dump */ + static char hex_buf[MAX_TRANS_UNIT * 2 + 1]; + mesh::Utils::toHex(hex_buf, raw, len); + printk("%s RAW: %s\n", getLogDateTime(), hex_buf); +#endif + (void)snr; + (void)rssi; +} + +void RepeaterMesh::logRx(mesh::Packet* pkt, int len, float score) { + if (_logging) { + LOG_INF("RX len=%d type=%d route=%s payload_len=%d SNR=%d RSSI=%d", + len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", + pkt->payload_len, (int)_radio->getLastSNR(), (int)_radio->getLastRSSI()); + } +} + +void RepeaterMesh::logTx(mesh::Packet* pkt, int len) { + if (_logging) { + LOG_INF("TX len=%d type=%d route=%s payload_len=%d", + len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", + pkt->payload_len); + } +} + +void RepeaterMesh::logTxFail(mesh::Packet* pkt, int len) { + if (_logging) { + LOG_WRN("TX FAIL len=%d type=%d route=%s payload_len=%d", + len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", + pkt->payload_len); + } +} + +/* Fast base^x approximation using IEEE 754 float bit tricks. + * Avoids linking pow()/powf() (~1.9KB). Accuracy ~5% which is + * more than sufficient for RX delay jitter calculation. */ +static float fast_powf(float base, float exp) +{ + /* log2(base) via IEEE 754: float bits ≈ 2^23 * (log2(x) + 127) */ + union { float f; uint32_t i; } bx = { .f = base }; + float log2_base = (float)(int32_t)(bx.i - 0x3F800000) * (1.0f / 8388608.0f); /* 1/2^23 */ + + /* exp2(exp * log2_base) via IEEE 754 */ + float y = exp * log2_base; + union { float f; uint32_t i; } ex; + ex.i = (uint32_t)((int32_t)(y * 8388608.0f) + 0x3F800000); + return ex.f; +} + +int RepeaterMesh::calcRxDelay(float score, uint32_t air_time) const { + if (_prefs.rx_delay_base <= 0.0f) return 0; + return (int)((fast_powf(_prefs.rx_delay_base, 0.85f - score) - 1.0f) * air_time); +} + +uint32_t RepeaterMesh::getRetransmitDelay(const mesh::Packet* packet) { + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor); + return getRNG()->nextInt(0, 3 * t + 1); +} + +uint32_t RepeaterMesh::getDirectRetransmitDelay(const mesh::Packet* packet) { + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); + return getRNG()->nextInt(0, 3 * t + 1); +} + +bool RepeaterMesh::filterRecvFloodPacket(mesh::Packet* pkt) { + LOG_INF("filterRecvFloodPacket: route_type=0x%02x wildcard_flags=0x%02x", + pkt->getRouteType(), region_map.getWildcard().flags); + if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { + recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); + LOG_INF("filterRecvFloodPacket: TRANSPORT_FLOOD -> recv_pkt_region=%s", + recv_pkt_region ? recv_pkt_region->name : "NULL"); + } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { + if (region_map.getWildcard().flags & REGION_DENY_FLOOD) { + recv_pkt_region = nullptr; + LOG_INF("filterRecvFloodPacket: FLOOD denied by wildcard"); + } else { + recv_pkt_region = ®ion_map.getWildcard(); + LOG_INF("filterRecvFloodPacket: FLOOD -> wildcard region '%s'", + recv_pkt_region->name); + } + } else { + recv_pkt_region = nullptr; + LOG_INF("filterRecvFloodPacket: not a flood packet -> NULL"); + } + return false; +} + +void RepeaterMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) { + if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { + uint32_t timestamp; + memcpy(×tamp, data, 4); + + data[len] = 0; + uint8_t reply_len; + + reply_path_len = -1; + if (data[4] == 0 || data[4] >= ' ') { + reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood()); + } else if (data[4] == ANON_REQ_TYPE_REGIONS && packet->isRouteDirect()) { + reply_len = handleAnonRegionsReq(sender, timestamp, &data[5]); + } else if (data[4] == ANON_REQ_TYPE_OWNER && packet->isRouteDirect()) { + reply_len = handleAnonOwnerReq(sender, timestamp, &data[5]); + } else if (data[4] == ANON_REQ_TYPE_BASIC && packet->isRouteDirect()) { + reply_len = handleAnonClockReq(sender, timestamp, &data[5]); + } else { + reply_len = 0; + } + + if (reply_len == 0) return; + + if (packet->isRouteFlood()) { + mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + } else if (reply_path_len < 0) { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY); + } + } +} + +int RepeaterMesh::searchPeersByHash(const uint8_t* hash) { + int n = 0; + for (int i = 0; i < acl.getNumClients(); i++) { + if (acl.getClientByIdx(i)->id.isHashMatch(hash)) { + matching_peer_indexes[n++] = i; + } + } + return n; +} + +void RepeaterMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { + int i = matching_peer_indexes[peer_idx]; + if (i >= 0 && i < acl.getNumClients()) { + memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE); + } +} + +static bool isShare(const mesh::Packet* packet) { + if (packet->hasTransportCodes()) { + return packet->transport_codes[0] == 0 && packet->transport_codes[1] == 0; + } + return false; +} + +void RepeaterMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, + const uint8_t* app_data, size_t app_data_len) { + mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); + + if (packet->path_len == 0 && !isShare(packet)) { + AdvertDataParser parser(app_data, app_data_len); + if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { + putNeighbour(id, timestamp, packet->getSNR()); + } + } +} + +void RepeaterMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, + const uint8_t* secret, uint8_t* data, size_t len) { + int i = matching_peer_indexes[sender_idx]; + if (i < 0 || i >= acl.getNumClients()) { + LOG_WRN("onPeerDataRecv: invalid peer idx: %d", i); + return; + } + ClientInfo* client = acl.getClientByIdx(i); + + if (type == PAYLOAD_TYPE_REQ) { + uint32_t timestamp; + memcpy(×tamp, data, 4); + + if (timestamp > client->last_timestamp) { + int reply_len = handleRequest(client, timestamp, &data[4], len - 4); + if (reply_len == 0) return; + + client->last_timestamp = timestamp; + client->last_activity = getRTCClock()->getCurrentTime(); + + if (packet->isRouteFlood()) { + mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); + if (reply) { + if (client->out_path_len >= 0) { + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); + } else { + sendFlood(reply, SERVER_RESPONSE_DELAY); + } + } + } + } else { + LOG_DBG("onPeerDataRecv: possible replay attack"); + } + } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->isAdmin()) { + uint32_t sender_timestamp; + memcpy(&sender_timestamp, data, 4); + uint8_t flags = (data[4] >> 2); + + if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + LOG_DBG("onPeerDataRecv: unsupported text type: flags=%02x", flags); + } else if (sender_timestamp >= client->last_timestamp) { + bool is_retry = (sender_timestamp == client->last_timestamp); + client->last_timestamp = sender_timestamp; + client->last_activity = getRTCClock()->getCurrentTime(); + + data[len] = 0; + + if (flags == TXT_TYPE_PLAIN) { + uint32_t ack_hash; + mesh::Utils::sha256((uint8_t*)&ack_hash, 4, data, 5 + strlen((char*)&data[5]), + client->id.pub_key, PUB_KEY_SIZE); + mesh::Packet* ack = createAck(ack_hash); + if (ack) { + if (client->out_path_len < 0) { + sendFlood(ack, TXT_ACK_DELAY); + } else { + sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); + } + } + } + + uint8_t temp[166]; + char* command = (char*)&data[5]; + char* reply = (char*)&temp[5]; + if (is_retry) { + *reply = 0; + } else { + handleCommand(sender_timestamp, command, reply); + } + + int text_len = strlen(reply); + if (text_len > 0) { + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + if (timestamp == sender_timestamp) timestamp++; + memcpy(temp, ×tamp, 4); + temp[4] = (TXT_TYPE_CLI_DATA << 2); + + auto reply_pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); + if (reply_pkt) { + if (client->out_path_len < 0) { + sendFlood(reply_pkt, CLI_REPLY_DELAY_MILLIS); + } else { + sendDirect(reply_pkt, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); + } + } + } + } else { + LOG_DBG("onPeerDataRecv: possible replay attack"); + } + } +} + +bool RepeaterMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, + uint8_t* path, uint8_t path_len, uint8_t extra_type, + uint8_t* extra, uint8_t extra_len) { + int i = matching_peer_indexes[sender_idx]; + if (i >= 0 && i < acl.getNumClients()) { + LOG_DBG("PATH to client, path_len=%d", path_len); + auto client = acl.getClientByIdx(i); + memcpy(client->out_path, path, client->out_path_len = path_len); + client->last_activity = getRTCClock()->getCurrentTime(); + } + return false; +} + +void RepeaterMesh::onControlDataRecv(mesh::Packet* packet) { + uint8_t type = packet->payload[0] & 0xF0; + if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 && + !_prefs.disable_fwd && discover_limiter.allow(getRTCClock()->getCurrentTime())) { + int i = 1; + uint8_t filter = packet->payload[i++]; + uint32_t tag; + memcpy(&tag, &packet->payload[i], 4); + i += 4; + uint32_t since = 0; + if (packet->payload_len >= i + 4) { + memcpy(&since, &packet->payload[i], 4); + i += 4; + } + + if ((filter & (1 << ADV_TYPE_REPEATER)) != 0 && _prefs.discovery_mod_timestamp >= since) { + bool prefix_only = packet->payload[0] & 1; + uint8_t data[6 + PUB_KEY_SIZE]; + data[0] = CTL_TYPE_NODE_DISCOVER_RESP | ADV_TYPE_REPEATER; + data[1] = packet->_snr; + memcpy(&data[2], &tag, 4); + memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE); + auto resp = createControlData(data, prefix_only ? 6 + 8 : 6 + PUB_KEY_SIZE); + if (resp) { + sendZeroHop(resp, getRetransmitDelay(resp) * 4); + } + } + } +} + +RepeaterMesh::RepeaterMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, + mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) + : mesh::Mesh(radio, ms, rng, rtc, *new mesh::StaticPoolPacketManager(), tables), + _board(board), + _cli(board, rtc, acl, &_prefs, this), + region_map(key_store), temp_map(key_store), + discover_limiter(4, 120), + anon_limiter(4, 180) { + + _store = nullptr; + last_millis = 0; + uptime_millis = 0; + next_local_advert = next_flood_advert = 0; + dirty_contacts_expiry = 0; + set_radio_at = revert_radio_at = 0; + _logging = false; + region_load_active = false; + recv_pkt_region = nullptr; + +#if MAX_NEIGHBOURS > 0 + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + neighbours[i].clear(); + } +#endif + + initNodePrefs(&_prefs); + strcpy(_prefs.node_name, "Repeater"); +} + +void RepeaterMesh::begin(RepeaterDataStore* store) { + mesh::Mesh::begin(); + _store = store; + + /* Load persisted data. + * NOTE: Identity is loaded in main_repeater.cpp before begin() is called, + * so we skip loading it here (self_id should already be set). */ + _store->loadPrefs(_prefs); + acl.load(_store->getAclPath(), self_id); + region_map.load(_store->getRegionsPath()); + + /* NOTE: Radio configuration is handled by SX126xRadio adapter using + * LoRaConfig defaults. The repeater uses the same radio params as companion. + * Dynamic radio reconfiguration (via CLI) is not yet supported - radio + * uses compile-time defaults from LoRaConfig. This avoids EBUSY errors + * from trying to reconfigure while radio is in async RX mode. */ + + updateAdvertTimer(); + updateFloodAdvertTimer(); + + _board.setAdcMultiplier(_prefs.adc_multiplier); + + LOG_INF("RepeaterMesh started: %s (freq=%.2f bw=%.0f sf=%d cr=%d)", + _prefs.node_name, (double)_prefs.freq, (double)_prefs.bw, _prefs.sf, _prefs.cr); +} + +void RepeaterMesh::savePrefs() { + if (_store) { + _store->savePrefs(_prefs); + } +} + +void RepeaterMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) { + set_radio_at = futureMillis(2000); + pending_freq = freq; + pending_bw = bw; + pending_sf = sf; + pending_cr = cr; + revert_radio_at = futureMillis(2000 + timeout_mins * 60 * 1000); +} + +bool RepeaterMesh::formatFileSystem() { + if (_store) { + return _store->formatFileSystem(); + } + return false; +} + +void RepeaterMesh::sendSelfAdvertisement(int delay_millis, bool flood) { + mesh::Packet* pkt = createSelfAdvert(); + if (pkt) { + if (flood) { + sendFlood(pkt, delay_millis); + } else { + sendZeroHop(pkt, delay_millis); + } + } else { + LOG_ERR("Unable to create advertisement packet"); + } +} + +void RepeaterMesh::updateAdvertTimer() { + if (_prefs.advert_interval > 0) { + next_local_advert = futureMillis(((uint32_t)_prefs.advert_interval) * 2 * 60 * 1000); + } else { + next_local_advert = 0; + } +} + +void RepeaterMesh::updateFloodAdvertTimer() { + if (_prefs.flood_advert_interval > 0) { + next_flood_advert = futureMillis(((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000); + } else { + next_flood_advert = 0; + } +} + +void RepeaterMesh::eraseLogFile() { + // Logging to file not implemented in Zephyr version + LOG_INF("Log erased"); +} + +void RepeaterMesh::dumpLogFile() { + // Logging to file not implemented in Zephyr version + LOG_INF("Log dump not implemented"); +} + +void RepeaterMesh::setTxPower(uint8_t power_dbm) { + radio_set_tx_power(power_dbm); +} + +void RepeaterMesh::formatNeighborsReply(char* reply) { + char* dp = reply; + +#if MAX_NEIGHBOURS > 0 + int16_t neighbours_count = 0; + NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS]; + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (neighbours[i].heard_timestamp > 0) { + sorted_neighbours[neighbours_count++] = &neighbours[i]; + } + } + + simple_sort(sorted_neighbours, (int)neighbours_count, + [](const NeighbourInfo* a, const NeighbourInfo* b) { + return a->heard_timestamp > b->heard_timestamp; + }); + + for (int i = 0; i < neighbours_count && dp - reply < 134; i++) { + NeighbourInfo* neighbour = sorted_neighbours[i]; + + if (i > 0) *dp++ = '\n'; + + char hex[10]; + mesh::Utils::toHex(hex, neighbour->id.pub_key, 4); + + uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; + sprintf(dp, "%s:%u:%d", hex, secs_ago, neighbour->snr); + while (*dp) dp++; + } +#endif + + if (dp == reply) { + strcpy(dp, "-none-"); + dp += 6; + } + *dp = 0; +} + +void RepeaterMesh::removeNeighbor(const uint8_t* pubkey, int key_len) { +#if MAX_NEIGHBOURS > 0 + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (memcmp(neighbours[i].id.pub_key, pubkey, key_len) == 0) { + neighbours[i].clear(); + } + } +#endif +} + +void RepeaterMesh::formatStatsReply(char* reply) { + StatsFormatHelper::formatCoreStats(reply, _board, *_ms, _err_flags, _mgr); +} + +void RepeaterMesh::formatRadioStatsReply(char* reply) { + auto& radio_driver = getRadioDriver(_radio); + StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime()); +} + +void RepeaterMesh::formatPacketStatsReply(char* reply) { + auto& radio_driver = getRadioDriver(_radio); + StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), + getNumRecvFlood(), getNumRecvDirect()); +} + +void RepeaterMesh::saveIdentity(const mesh::LocalIdentity& new_id) { + if (_store) { + _store->saveIdentity(new_id); + } +} + +void RepeaterMesh::clearStats() { + auto& radio_driver = getRadioDriver(_radio); + radio_driver.resetStats(); + resetStats(); + /* Note: SimpleMeshTables doesn't have resetStats in Zephyr version */ +} + +void RepeaterMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + if (region_load_active) { + if (StrHelper::isBlank(command)) { + region_map = temp_map; + region_load_active = false; + sprintf(reply, "OK - loaded %d regions", region_map.getCount()); + } else { + char* np = command; + while (*np == ' ') np++; + int indent = np - command; + + char* ep = np; + while (RegionMap::is_name_char(*ep)) ep++; + if (*ep) { *ep++ = 0; } + + while (*ep && *ep != 'F') ep++; + + if (indent > 0 && indent < 8 && strlen(np) > 0) { + auto parent = load_stack[indent - 1]; + if (parent) { + auto old = region_map.findByName(np); + auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); + if (nw) { + nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD); + load_stack[indent] = nw; + } + } + } + reply[0] = 0; + } + return; + } + + while (*command == ' ') command++; + + if (strlen(command) > 4 && command[2] == '|') { + memcpy(reply, command, 3); + reply += 3; + command += 3; + } + + // ACL commands - supports BOTH formats for app compatibility: + // Old Arduino: setperm {pubkey-hex} {permissions} (pubkey is long, perms is short) + // MeshCore App: setperm {permissions} {pubkey-hex} (perms is short 2-char hex, pubkey is long) + // Detection: if first part is <= 2 chars, it's permissions; otherwise it's pubkey + if (memcmp(command, "setperm ", 8) == 0) { + char* first = &command[8]; + char* sp = strchr(first, ' '); + if (sp == nullptr) { + strcpy(reply, "Err - bad params"); + } else { + *sp++ = 0; // null terminate first part + char* second = sp; + + // Detect format: if first part is short (1-2 chars), it's permissions + int first_len = strlen(first); + char* hex; + uint8_t perms; + + if (first_len <= 2) { + // App format: setperm {perms} {pubkey} + perms = (uint8_t)strtol(first, nullptr, 16); + hex = second; + } else { + // Arduino format: setperm {pubkey} {perms} + hex = first; + perms = (uint8_t)atoi(second); + } + + uint8_t pubkey[PUB_KEY_SIZE]; + int hex_len = strlen(hex); + if (hex_len > PUB_KEY_SIZE * 2) hex_len = PUB_KEY_SIZE * 2; + if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) { + if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) { + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - invalid params"); + } + } else { + strcpy(reply, "Err - bad pubkey"); + } + } + } else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) { + LOG_INF("ACL:"); + for (int i = 0; i < acl.getNumClients(); i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0) continue; + char hex[PUB_KEY_SIZE * 2 + 1]; + mesh::Utils::toHex(hex, c->id.pub_key, PUB_KEY_SIZE); + LOG_INF(" %02X %s", c->permissions, hex); + } + reply[0] = 0; + } else if (memcmp(command, "region", 6) == 0) { + reply[0] = 0; + const char* parts[4]; + int n = mesh::Utils::parseTextParts(command, parts, 4, ' '); + + if (n == 1) { + region_map.exportTo(reply, 160); + } else if (n >= 2 && strcmp(parts[1], "load") == 0) { + temp_map.resetFrom(region_map); + memset(load_stack, 0, sizeof(load_stack)); + load_stack[0] = &temp_map.getWildcard(); + region_load_active = true; + } else if (n >= 2 && strcmp(parts[1], "save") == 0) { + _prefs.discovery_mod_timestamp = getRTCClock()->getCurrentTime(); + savePrefs(); + bool success = region_map.save(_store->getRegionsPath()); + strcpy(reply, success ? "OK" : "Err - save failed"); + } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) { + auto region = region_map.findByNamePrefix(parts[2]); + if (region) { + region->flags &= ~REGION_DENY_FLOOD; + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) { + auto region = region_map.findByNamePrefix(parts[2]); + if (region) { + region->flags |= REGION_DENY_FLOOD; + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n >= 3 && strcmp(parts[1], "get") == 0) { + auto region = region_map.findByNamePrefix(parts[2]); + if (region) { + auto parent = region_map.findById(region->parent); + if (parent && parent->id != 0) { + sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); + } else { + sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); + } + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n >= 3 && strcmp(parts[1], "home") == 0) { + auto home = region_map.findByNamePrefix(parts[2]); + if (home) { + region_map.setHomeRegion(home); + sprintf(reply, " home is now %s", home->name); + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n == 2 && strcmp(parts[1], "home") == 0) { + auto home = region_map.getHomeRegion(); + sprintf(reply, " home is %s", home ? home->name : "*"); + } else if (n >= 3 && strcmp(parts[1], "put") == 0) { + auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : ®ion_map.getWildcard(); + if (parent == nullptr) { + strcpy(reply, "Err - unknown parent"); + } else { + auto region = region_map.putRegion(parts[2], parent->id); + if (region == nullptr) { + strcpy(reply, "Err - unable to put"); + } else { + strcpy(reply, "OK"); + } + } + } else if (n >= 3 && strcmp(parts[1], "remove") == 0) { + auto region = region_map.findByName(parts[2]); + if (region) { + if (region_map.removeRegion(*region)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - not empty"); + } + } else { + strcpy(reply, "Err - not found"); + } + } else if (n >= 3 && strcmp(parts[1], "list") == 0) { + uint8_t mask = 0; + bool invert = false; + if (strcmp(parts[2], "allowed") == 0) { + mask = REGION_DENY_FLOOD; + invert = false; + } else if (strcmp(parts[2], "denied") == 0) { + mask = REGION_DENY_FLOOD; + invert = true; + } else { + strcpy(reply, "Err - use 'allowed' or 'denied'"); + return; + } + int len = region_map.exportNamesTo(reply, 160, mask, invert); + if (len == 0) { + strcpy(reply, "-none-"); + } + } else { + strcpy(reply, "Err - ??"); + } + } else { + _cli.handleCommand(sender_timestamp, command, reply); + } +} + +void RepeaterMesh::loop() { + mesh::Mesh::loop(); + + if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { + mesh::Packet* pkt = createSelfAdvert(); + if (pkt) sendFlood(pkt); + updateFloodAdvertTimer(); + updateAdvertTimer(); + } else if (next_local_advert && millisHasNowPassed(next_local_advert)) { + mesh::Packet* pkt = createSelfAdvert(); + if (pkt) sendZeroHop(pkt); + updateAdvertTimer(); + } + + if (set_radio_at && millisHasNowPassed(set_radio_at)) { + set_radio_at = 0; + getRadioDriver(_radio).reconfigureWithParams(pending_freq, pending_bw, pending_sf, pending_cr); + LOG_INF("Temp radio params applied"); + } + + if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { + revert_radio_at = 0; + getRadioDriver(_radio).reconfigureWithParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + LOG_INF("Radio params restored"); + } + + if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { + acl.save(_store->getAclPath()); + dirty_contacts_expiry = 0; + } + + uint32_t now = k_uptime_get(); + uptime_millis += now - last_millis; + last_millis = now; +} + +bool RepeaterMesh::hasPendingWork() const { + return _mgr->getOutboundCount(0xFFFFFFFF) > 0; +} diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h new file mode 100644 index 0000000..a92f6f0 --- /dev/null +++ b/zephcore/app/RepeaterMesh.h @@ -0,0 +1,189 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * RepeaterMesh - LoRa mesh repeater implementation + * + * Extends mesh::Mesh with: + * - ACL-based client authentication + * - Region-based flood filtering + * - Neighbor tracking + * - CLI commands (via USB serial) + * - Protocol handlers (login, status, telemetry, etc.) + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "RepeaterDataStore.h" + +#ifndef FIRMWARE_VERSION + #define FIRMWARE_VERSION "v1.12.0-zephyr" +#endif + +#ifndef FIRMWARE_BUILD_DATE + #define FIRMWARE_BUILD_DATE __DATE__ +#endif + +#define FIRMWARE_ROLE "repeater" + +#ifndef MAX_NEIGHBOURS + #ifdef CONFIG_ZEPHCORE_MAX_NEIGHBOURS + #define MAX_NEIGHBOURS CONFIG_ZEPHCORE_MAX_NEIGHBOURS + #else + #define MAX_NEIGHBOURS 16 + #endif +#endif + +struct NeighbourInfo { + mesh::Identity id; + uint32_t advert_timestamp; + uint32_t heard_timestamp; + int8_t snr; // multiplied by 4 + + void clear() { + id = mesh::Identity(); + advert_timestamp = 0; + heard_timestamp = 0; + snr = 0; + } +}; + +struct RepeaterStats { + uint16_t batt_milli_volts; + uint16_t curr_tx_queue_len; + int16_t noise_floor; + int16_t last_rssi; + uint32_t n_packets_recv; + uint32_t n_packets_sent; + uint32_t total_air_time_secs; + uint32_t total_up_time_secs; + uint32_t n_sent_flood, n_sent_direct; + uint32_t n_recv_flood, n_recv_direct; + uint16_t err_events; + int16_t last_snr; // x 4 + uint16_t n_direct_dups, n_flood_dups; + uint32_t total_rx_air_time_secs; + uint32_t n_recv_errors; +}; + +class RepeaterMesh : public mesh::Mesh, public CommonCLICallbacks { + mesh::MainBoard& _board; + RepeaterDataStore* _store; + uint32_t last_millis; + uint64_t uptime_millis; + unsigned long next_local_advert, next_flood_advert; + bool _logging; + NodePrefs _prefs; + ClientACL acl; + CommonCLI _cli; + uint8_t reply_data[MAX_PACKET_PAYLOAD]; + uint8_t reply_path[MAX_PATH_SIZE]; + int8_t reply_path_len; + TransportKeyStore key_store; + RegionMap region_map, temp_map; + RegionEntry* load_stack[8]; + RegionEntry* recv_pkt_region; + RateLimiter discover_limiter, anon_limiter; + bool region_load_active; + unsigned long dirty_contacts_expiry; +#if MAX_NEIGHBOURS > 0 + NeighbourInfo neighbours[MAX_NEIGHBOURS]; +#endif + unsigned long set_radio_at, revert_radio_at; + float pending_freq; + float pending_bw; + uint8_t pending_sf; + uint8_t pending_cr; + int matching_peer_indexes[MAX_CLIENTS]; + + void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); + uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); + uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); + uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); + uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); + int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); + mesh::Packet* createSelfAdvert(); + +protected: + uint8_t getDutyCyclePercent() const override { + return (uint8_t)_prefs.airtime_factor; + } + + bool allowPacketForward(const mesh::Packet* packet) override; + const char* getLogDateTime() override; + + void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; + void logRx(mesh::Packet* pkt, int len, float score) override; + void logTx(mesh::Packet* pkt, int len) override; + void logTxFail(mesh::Packet* pkt, int len) override; + int calcRxDelay(float score, uint32_t air_time) const override; + + uint32_t getRetransmitDelay(const mesh::Packet* packet) override; + uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + + int getInterferenceThreshold() const override { + return _prefs.interference_threshold; + } + int getAGCResetInterval() const override { + return ((int)_prefs.agc_reset_interval) * 4000; + } + uint8_t getExtraAckTransmitCount() const override { + return _prefs.multi_acks; + } + + bool filterRecvFloodPacket(mesh::Packet* pkt) override; + + void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; + int searchPeersByHash(const uint8_t* hash) override; + void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; + void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len); + void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override; + bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; + void onControlDataRecv(mesh::Packet* packet) override; + +public: + RepeaterMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, + mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); + + void begin(RepeaterDataStore* store); + + /* CommonCLICallbacks */ + const char* getFirmwareVer() override { return FIRMWARE_VERSION; } + const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } + const char* getRole() override { return FIRMWARE_ROLE; } + const char* getNodeName() { return _prefs.node_name; } + NodePrefs* getNodePrefs() { return &_prefs; } + + void savePrefs() override; + void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override; + bool formatFileSystem() override; + void sendSelfAdvertisement(int delay_millis, bool flood) override; + void updateAdvertTimer() override; + void updateFloodAdvertTimer() override; + void setLoggingOn(bool enable) override { _logging = enable; } + void eraseLogFile() override; + void dumpLogFile() override; + void setTxPower(uint8_t power_dbm) override; + void formatNeighborsReply(char* reply) override; + void removeNeighbor(const uint8_t* pubkey, int key_len) override; + void formatStatsReply(char* reply) override; + void formatRadioStatsReply(char* reply) override; + void formatPacketStatsReply(char* reply) override; + + mesh::LocalIdentity& getSelfId() override { return self_id; } + void saveIdentity(const mesh::LocalIdentity& new_id) override; + void clearStats() override; + + void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void loop(); + + bool hasPendingWork() const; +}; diff --git a/zephcore/boards/common/esp32_common.conf b/zephcore/boards/common/esp32_common.conf new file mode 100644 index 0000000..18efef8 --- /dev/null +++ b/zephcore/boards/common/esp32_common.conf @@ -0,0 +1,21 @@ +# ESP32 Platform Configuration +# Included by all ESP32-based boards (XIAO ESP32-C3, XIAO ESP32-C6, etc.) +# Only contains ESP32-specific settings — common BLE/Storage/Input is in zephcore_common.conf +# +# Include order: prj.conf → zephcore_common.conf → esp32_common.conf → /board.conf + +# ========== BLE: ESP32-specific controller settings ========== +# DLE managed internally by Espressif blob — no CONFIG_BT_CTLR_DATA_LENGTH_MAX +# TX power managed internally by Espressif blob — no CONFIG_BT_CTLR_TX_PWR_* + +# BLE thread stacks — ESP32 software BLE controller needs larger stacks +CONFIG_BT_TX_PROCESSOR_STACK_SIZE=2048 + +# ========== Heap ========== +# ESP32 BLE stack requires larger heap (override zephcore_common 2KB default) +CONFIG_HEAP_MEM_POOL_SIZE=32768 + +# ========== Debug ========== +# Generic Zephyr assert (ESP32 uses Espressif BLE blob, not sw_split controller, +# so BT_CTLR_ASSERT_HANDLER is not available) +CONFIG_ASSERT=y diff --git a/zephcore/boards/common/filesystem.dtsi b/zephcore/boards/common/filesystem.dtsi new file mode 100644 index 0000000..e2ab59b --- /dev/null +++ b/zephcore/boards/common/filesystem.dtsi @@ -0,0 +1,27 @@ +/* + * Common LittleFS filesystem configuration for ZephCore boards. + * Include this in board overlays to get standard /lfs mount. + * + * Requires board to define: &lfs_partition + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + fstab { + compatible = "zephyr,fstab"; + + /* Internal LittleFS for identity, prefs */ + lfs: lfs { + compatible = "zephyr,fstab,littlefs"; + mount-point = "/lfs"; + partition = <&lfs_partition>; + automount; + read-size = <16>; + prog-size = <16>; + cache-size = <64>; + lookahead-size = <32>; + block-cycles = <512>; + }; + }; +}; diff --git a/zephcore/boards/common/logging.conf b/zephcore/boards/common/logging.conf new file mode 100644 index 0000000..e08143b --- /dev/null +++ b/zephcore/boards/common/logging.conf @@ -0,0 +1,21 @@ +# Logging Tuning — included automatically for debug builds only. +# Prod builds (CONFIG_LOG=n via prod.conf) skip this file entirely, +# avoiding Kconfig warnings from log settings that depend on CONFIG_LOG. + +# ZephCore BLE modules at INF level (connection lifecycle, pairing, TX/RX) +# Default level is WRN — this ensures our INF messages come through. +# Avoid DBG level — it causes system workqueue stack overflow. +CONFIG_ZEPHCORE_BLE_LOG_LEVEL_INF=y + +# Deferred mode (non-blocking) — this is the Kconfig default, but explicit for clarity +CONFIG_LOG_MODE_DEFERRED=y +CONFIG_LOG_BUFFER_SIZE=8192 +CONFIG_LOG_PROCESS_TRIGGER_THRESHOLD=1 +CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048 + +# Backends +CONFIG_LOG_BACKEND_UART=y +CONFIG_LOG_BACKEND_RTT=y + +# Suppress noisy subsystem logs +CONFIG_CFB_LOG_LEVEL_ERR=y diff --git a/zephcore/boards/common/mg24_common.conf b/zephcore/boards/common/mg24_common.conf new file mode 100644 index 0000000..fe8cfa5 --- /dev/null +++ b/zephcore/boards/common/mg24_common.conf @@ -0,0 +1,26 @@ +# EFR32MG24 Platform Configuration +# Included by all MG24-based boards (XIAO MG24, etc.) +# Only contains MG24-specific settings — common BLE/Storage/Input is in zephcore_common.conf +# +# Include order: prj.conf → zephcore_common.conf → mg24_common.conf → /board.conf +# +# NOTE: MG24 uses Silicon Labs proprietary BLE controller blob. +# Requires: west blobs fetch hal_silabs + +# ========== BLE: MG24-specific controller settings ========== +# DLE managed internally by Silabs blob — no CONFIG_BT_CTLR_DATA_LENGTH_MAX +# TX power managed internally by Silabs blob — no CONFIG_BT_CTLR_TX_PWR_* + +# BLE thread stacks — Silabs blob needs larger stacks +CONFIG_BT_TX_PROCESSOR_STACK_SIZE=2048 + +# Controller assert handler — without this, a controller assert silently +# freezes the CPU at highest IRQ priority with zero log output. +CONFIG_BT_CTLR_ASSERT_HANDLER=y + +# ========== Debug ========== +CONFIG_ASSERT=y + +# ========== Heap ========== +# Silabs BLE stack requires larger heap (override zephcore_common 2KB default) +CONFIG_HEAP_MEM_POOL_SIZE=32768 diff --git a/zephcore/boards/common/nrf52_common.conf b/zephcore/boards/common/nrf52_common.conf new file mode 100644 index 0000000..1effc7c --- /dev/null +++ b/zephcore/boards/common/nrf52_common.conf @@ -0,0 +1,35 @@ +# nRF52 Platform Configuration +# Included by all nRF52-based boards (RAK4631, Wio Tracker, T1000-E, Ikoka Nano, etc.) +# Only contains nRF52-specific settings — common BLE/Storage/Input is in zephcore_common.conf +# +# Include order: prj.conf → zephcore_common.conf → nrf52_common.conf → /board.conf + +# ========== Build Output (nRF52 specific) ========== +CONFIG_BUILD_OUTPUT_UF2=y +CONFIG_USE_DT_CODE_PARTITION=y + +# ========== USB CDC ACM (nRF52 USB) ========== +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=y +CONFIG_CDC_ACM_SERIAL_PRODUCT_STRING="ZephCore" +CONFIG_UART_LINE_CTRL=y + +# ========== BLE: nRF52-specific controller settings ========== +# TX power: +4 dBm (Zephyr open-source BLE controller) +CONFIG_BT_CTLR_TX_PWR_PLUS_4=y + +# Data Length Extension — controller-side max (host-side in zephcore_common.conf) +CONFIG_BT_CTLR_DATA_LENGTH_MAX=251 + +# ========== Flash: nRF52-specific ========== +# Recover from corrupted NVS sectors (nRF52 flash can develop bad regions) +CONFIG_NVS_INIT_BAD_MEMORY_REGION=y + +# ========== SEGGER RTT (J-Link debug) ========== +# RTT log backend is in boards/common/logging.conf (only included for debug builds). +CONFIG_USE_SEGGER_RTT=y +CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096 + +# ========== Debug ========== +CONFIG_ASSERT=y +CONFIG_BT_CTLR_ASSERT_HANDLER=y diff --git a/zephcore/boards/common/nrf54l_common.conf b/zephcore/boards/common/nrf54l_common.conf new file mode 100644 index 0000000..a30b9da --- /dev/null +++ b/zephcore/boards/common/nrf54l_common.conf @@ -0,0 +1,30 @@ +# nRF54L Platform Configuration +# Included by all nRF54L-based boards (XIAO nRF54L15, etc.) +# Only contains nRF54L-specific settings — common BLE/Storage/Input is in zephcore_common.conf +# +# Include order: prj.conf → zephcore_common.conf → nrf54l_common.conf → /board.conf +# +# NOTE: nRF54L15 has NO native USB. +# XIAO nRF54L15 has a SAMD11 bridge chip (CMSIS-DAP) that converts UART20 → USB CDC +# on the host side. Flashing is via SWD through CMSIS-DAP (west flash / openocd). + +# ========== CPU Clock: DVFS low (64 MHz) for power saving ========== +# nRF54L15 supports 64/128/320 MHz via HSFLL DVFS. +# 64 MHz is plenty for event-driven companion firmware. +CONFIG_CLOCK_CONTROL_NRF_HSFLL_LOCAL_REQ_LOW_FREQ=y + +# ========== BLE: nRF54L-specific controller settings ========== +# TX power: +4 dBm (same open-source BLE controller as nRF52) +CONFIG_BT_CTLR_TX_PWR_PLUS_4=y + +# Data Length Extension — controller-side max +CONFIG_BT_CTLR_DATA_LENGTH_MAX=251 + +# ========== SEGGER RTT (J-Link / CMSIS-DAP debug) ========== +CONFIG_LOG_BACKEND_RTT=y +CONFIG_USE_SEGGER_RTT=y +CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096 + +# ========== Debug ========== +CONFIG_ASSERT=y +CONFIG_BT_CTLR_ASSERT_HANDLER=y diff --git a/zephcore/boards/common/packet_logging.conf b/zephcore/boards/common/packet_logging.conf new file mode 100644 index 0000000..439489b --- /dev/null +++ b/zephcore/boards/common/packet_logging.conf @@ -0,0 +1,34 @@ +# Packet Logging Only - Common to Repeater boards +# Build: west build -b zephcore --pristine -- \ +# -DCONFIG_ZEPHCORE_ROLE_REPEATER=y \ +# -DEXTRA_CONF_FILE="boards/common/packet_logging.conf" +# +# Output: Clean packet logs only (RAW, RX, TX lines) +# No debug spam from zephcore_lora, zephcore_dispatcher, etc. +# +# Example output: +# 14:01:24 - 10/2/2026 U RAW: 150341E248D93B1BA948BC7E... +# 14:01:24 - 10/2/2026 U: RX, len=40 (type=5, route=F, payload_len=35) SNR=1 RSSI=-106 score=949 time=476 hash=C3CD323A5235CB60 + +# Enable logging subsystem (needed for LOG_* macros) +CONFIG_LOG=y +CONFIG_LOG_MODE_DEFERRED=y + +# Send printk directly to console (USB CDC), not through log system +# This avoids needing CONFIG_LOG_BACKEND_UART +CONFIG_LOG_PRINTK=n + +# Enable packet logging +CONFIG_ZEPHCORE_PACKET_LOGGING=y +CONFIG_ZEPHCORE_PACKET_LOGGING_ONLY=y + +# Silence all module logs (packet logs use printk, bypass log levels) +CONFIG_ZEPHCORE_LORA_LOG_LEVEL_OFF=y +CONFIG_ZEPHCORE_BLE_LOG_LEVEL_OFF=y +CONFIG_ZEPHCORE_BOARD_LOG_LEVEL_OFF=y +CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL_OFF=y +CONFIG_ZEPHCORE_SENSORS_LOG_LEVEL_OFF=y +CONFIG_ZEPHCORE_GPS_LOG_LEVEL_OFF=y +CONFIG_ZEPHCORE_USB_LOG_LEVEL_OFF=y +CONFIG_ZEPHCORE_UI_ACTIONS_LOG_LEVEL_OFF=y +CONFIG_LORA_LOG_LEVEL_OFF=y diff --git a/zephcore/boards/common/prod.conf b/zephcore/boards/common/prod.conf new file mode 100644 index 0000000..3a5b101 --- /dev/null +++ b/zephcore/boards/common/prod.conf @@ -0,0 +1,17 @@ +# Production Config - Common to ALL boards (Companion & Repeater) +# Build: west build -b zephcore --pristine -- \ +# -DEXTRA_CONF_FILE="boards/common/prod.conf" +# +# Disables logging for smaller binary and lower power consumption. +# USB CDC still works for CLI commands on repeaters. + +# Disable logging subsystem (saves ~62KB flash, ~2-5mA power) +CONFIG_LOG=n +CONFIG_LOG_BACKEND_UART=n +CONFIG_LOG_BACKEND_RTT=n + +# Disable asserts (saves flash, removes CMake warning) +CONFIG_ASSERT=n + +# Max contacts for production +CONFIG_ZEPHCORE_MAX_CONTACTS=500 diff --git a/zephcore/boards/common/qspi-ext.dtsi b/zephcore/boards/common/qspi-ext.dtsi new file mode 100644 index 0000000..ad47652 --- /dev/null +++ b/zephcore/boards/common/qspi-ext.dtsi @@ -0,0 +1,25 @@ +/* + * Common QSPI external flash LittleFS configuration for ZephCore boards. + * Include this in board overlays that have QSPI flash for /ext mount. + * + * Requires board to define: &qspi_storage_partition + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + fstab { + /* External QSPI for contacts, channels, blobs */ + qspi_lfs: qspi_lfs { + compatible = "zephyr,fstab,littlefs"; + mount-point = "/ext"; + partition = <&qspi_storage_partition>; + automount; + read-size = <16>; + prog-size = <16>; + cache-size = <64>; + lookahead-size = <32>; + block-cycles = <512>; + }; + }; +}; diff --git a/zephcore/boards/common/repeater.conf b/zephcore/boards/common/repeater.conf new file mode 100644 index 0000000..42fe28b --- /dev/null +++ b/zephcore/boards/common/repeater.conf @@ -0,0 +1,7 @@ +# Repeater-specific config +# +# Build with: -DEXTRA_CONF_FILE="boards/common/repeater.conf" +# +# Disable Zephyr's auto USB init - we do it ourselves with 1200 baud touch detection +# for entering UF2 bootloader mode (like Arduino's behavior). +CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n diff --git a/zephcore/boards/common/sensors-i2c.dtsi b/zephcore/boards/common/sensors-i2c.dtsi new file mode 100644 index 0000000..db83ea2 --- /dev/null +++ b/zephcore/boards/common/sensors-i2c.dtsi @@ -0,0 +1,105 @@ +/* + * Common I2C Sensor Definitions for ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Include this file inside an &i2cN { ... } block to define all + * supported sensor nodes on that bus. Sensors that are not physically + * present will fail device_is_ready() at runtime and be gracefully + * skipped by ZephyrEnvSensors — no harm, no extra current draw. + * + * Usage (board overlay): + * &i2c0 { + * #include "../../common/sensors-i2c.dtsi" + * }; + * + * ─── Environment Sensors (temperature / humidity / pressure) ─── + * + * Sensor │ Address │ Compatible │ Channels + * ────────────┼─────────┼─────────────────────┼──────────────────── + * SHTC3 │ 0x70 │ sensirion,shtcx │ temp + humidity + * AHT20 │ 0x38 │ aosong,aht20 │ temp + humidity + * DHT20 │ 0x38 │ aosong,dht20 │ temp + humidity + * AM2301B │ 0x38 │ aosong,am2301b │ temp + humidity + * SHT4x │ 0x44 │ sensirion,sht4x │ temp + humidity + * SHT3xD │ 0x44 │ sensirion,sht3xd │ temp + humidity + * BME280 │ 0x76 │ bosch,bme280 │ temp + humidity + pressure + * BME680 │ 0x76 │ bosch,bme680 │ temp + humidity + pressure + gas + * BMP280 │ 0x77 │ bosch,bme280 │ temp + pressure (BME280 driver) + * BMP388 │ 0x77 │ bosch,bmp388 │ temp + pressure + * LPS22HB │ 0x5C │ st,lps22hb-press │ pressure + temp + * + * Note: AHT20/DHT20/AM2301B share address 0x38 — only one can be + * defined. We use AHT20 as the default (same driver handles all). + * + * Note: SHT4x and SHT3xD share address 0x44 — only one can be + * defined. We use SHT4x as the default (more common in new designs). + * + * Note: BME280 and BME680 share address 0x76 — only one can be + * defined. We use BME280 as default (its driver also handles BMP280). + * Boards with BME680 should override this node in their overlay. + * + * Note: BMP280 uses the BME280 driver (same register set), at alt + * address 0x77. BMP388 also uses 0x77. Only one can be defined — + * we use BMP388 since it's the newer part. + * + * ─── Power Monitor Sensors (voltage / current / power) ─── + * + * Sensor │ Address │ Compatible │ Channels + * ────────────┼─────────┼──────────────┼──────────────────── + * INA219 │ 0x40 │ ti,ina219 │ 1ch: V + I + P + * INA3221 │ 0x40 │ ti,ina3221 │ 3ch: V + I + P + * INA226 │ 0x40 │ ti,ina226 │ 1ch: V + I + P + * + * Note: INA219, INA3221, and INA226 share address 0x40 — only one + * can be defined at a time. Boards with a specific INA chip should + * delete the others or override this file. + * We define INA219 as default (most common single-channel monitor). + */ + +/* ── Temperature / Humidity ── */ + +shtc3: shtc3@70 { + compatible = "sensirion,shtcx"; + reg = <0x70>; + measure-mode = "normal"; + clock-stretching; +}; + +aht20: aht20@38 { + compatible = "aosong,aht20"; + reg = <0x38>; +}; + +sht4x: sht4x@44 { + compatible = "sensirion,sht4x"; + reg = <0x44>; + repeatability = <2>; /* high */ +}; + +/* ── Combo: Temperature + Humidity + Pressure ── */ + +bme280: bme280@76 { + compatible = "bosch,bme280"; + reg = <0x76>; +}; + +/* ── Pressure (standalone) ── */ + +lps22hb: lps22hb@5c { + compatible = "st,lps22hb-press"; + reg = <0x5c>; +}; + +bmp388: bmp388@77 { + compatible = "bosch,bmp388"; + reg = <0x77>; +}; + +/* ── Power Monitors ── */ + +ina219: ina219@40 { + compatible = "ti,ina219"; + reg = <0x40>; + shunt-milliohm = <100>; + lsb-microamp = <10>; +}; diff --git a/zephcore/boards/common/zephcore_common.conf b/zephcore/boards/common/zephcore_common.conf new file mode 100644 index 0000000..b874963 --- /dev/null +++ b/zephcore/boards/common/zephcore_common.conf @@ -0,0 +1,157 @@ +# ZephCore Common Configuration +# Included by ALL boards regardless of MCU (nRF52, nRF54L, ESP32, MG24, etc.) +# Contains application-level settings and platform-agnostic BLE/storage/radio config. +# +# Platform-specific configs (nrf52_common.conf, esp32_common.conf, etc.) only +# contain overrides and additions unique to that platform. + +# ========== Core Settings ========== +CONFIG_MAIN_STACK_SIZE=8192 +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=8192 +CONFIG_EVENTS=y + +CONFIG_CPP=y +CONFIG_STD_CPP17=y + +# ========== Printf Formatting ========== +# Enable floating point support in printf/sprintf (required for CLI) +CONFIG_CBPRINTF_FP_SUPPORT=y + +# ========== Crypto (mbedTLS) ========== +# Required for mesh encryption (AES-ECB, SHA256, HMAC) +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y +CONFIG_MBEDTLS_SHA256=y +CONFIG_MBEDTLS_MD_C=y + +# ========== Stack Protection ========== +CONFIG_HW_STACK_PROTECTION=y +CONFIG_THREAD_NAME=y + +# ========== Hardware Basics ========== +CONFIG_ADC=y +CONFIG_HWINFO=y +CONFIG_REBOOT=y + +# ========== LoRa ========== +CONFIG_LORA=y +CONFIG_REGULATOR=y + +# ========== Display Subsystem ========== +# When no display is in devicetree, no driver is instantiated — zero overhead. +CONFIG_DISPLAY=y +CONFIG_CHARACTER_FRAMEBUFFER=y + +# Heap for CFB framebuffer allocation (cfb_framebuffer_init uses k_malloc). +# 2KB baseline — platforms with larger BLE stacks override this. +CONFIG_HEAP_MEM_POOL_SIZE=2048 + +# ========== Sensor Subsystem ========== +# Sensors are auto-detected at runtime via I2C scan +CONFIG_SENSOR=y +CONFIG_I2C=y + +# ========== GNSS Subsystem ========== +# Driver auto-selected from board DTS compatible (zero overhead if no GNSS in DT): +# quectel,lc76g -> Quectel LCx6G driver +# luatos,air530z -> LuatOS Air530Z driver +# gnss-nmea-generic -> passive NMEA parser +CONFIG_GNSS=y +CONFIG_MODEM_MODULES=y + +# ========== BLE Stack (common to ALL platforms) ========== +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_ZEPHYR_NUS=y +CONFIG_BT_ZEPHYR_NUS_DEFAULT_INSTANCE=n +CONFIG_BT_DEVICE_NAME="MeshCore" +CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_DEVICE_NAME_MAX=24 +CONFIG_BT_DEVICE_NAME_GATT_WRITABLE=y +CONFIG_BT_DEVICE_APPEARANCE=0 +CONFIG_BT_MAX_CONN=1 + +# SMP: MITM passkey, bonding, enforce bonding +CONFIG_BT_SMP=y +CONFIG_BT_SMP_ENFORCE_MITM=y +CONFIG_BT_APP_PASSKEY=y +CONFIG_BT_BONDABLE=y +CONFIG_BT_BONDING_REQUIRED=y +CONFIG_BT_MAX_PAIRED=5 +CONFIG_BT_KEYS_OVERWRITE_OLDEST=y + +# Connection parameter update +CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=y + +# MTU max (controller limit 251, L2CAP MTU 247, ATT payload 244) +CONFIG_BT_BUF_ACL_RX_SIZE=251 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_L2CAP_TX_MTU=247 + +# Data Length Extension update support (host side) +CONFIG_BT_USER_DATA_LEN_UPDATE=y + +# Privacy - use Resolvable Private Addresses (Apple requirement) +CONFIG_BT_PRIVACY=y +CONFIG_BT_RPA_TIMEOUT=900 +# NOTE: SC_PAIR_ONLY intentionally NOT set — Windows can't handle SC passkey +# pairing (never prompts for PIN). Allowing both Legacy and SC lets Windows +# pair with Legacy while iOS/Android use SC. + +# Disable Extended Advertising (ALL boards) +# Some controllers (MG24, ESP32) auto-select ext adv PDUs. +# Web Bluetooth (Chrome) can't discover devices using ext adv PDUs. +# Legacy ADV_IND works universally with all BLE scanners and web apps. +CONFIG_BT_EXT_ADV=n + +# Device Information Service (Apple requirement) +CONFIG_BT_DIS=y +CONFIG_BT_DIS_MANUF_NAME=y +CONFIG_BT_DIS_MANUF_NAME_STR="ZephCore" +CONFIG_BT_DIS_MODEL_NUMBER=y +CONFIG_BT_DIS_MODEL_NUMBER_STR="ZephCore" +CONFIG_BT_DIS_FW_REV=y +CONFIG_BT_DIS_FW_REV_STR="1.13.0" +CONFIG_BT_DIS_SW_REV=y +CONFIG_BT_DIS_SW_REV_STR="Zephyr" +CONFIG_BT_DIS_PNP=n + +# Persistent bonds (NVS) +CONFIG_BT_SETTINGS=y +CONFIG_SETTINGS_RUNTIME=y + +# ========== Flash & Storage (common to ALL platforms) ========== +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_NVS=y +CONFIG_SETTINGS=y +CONFIG_SETTINGS_NVS=y +CONFIG_SETTINGS_NVS_SECTOR_COUNT=2 +CONFIG_FLASH_PAGE_LAYOUT=y + +# LittleFS for DataStore +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_LITTLEFS=y +CONFIG_FS_LITTLEFS_NUM_FILES=8 +CONFIG_FS_LITTLEFS_NUM_DIRS=4 +CONFIG_FILE_SYSTEM_MKFS=y + +# ========== Input Subsystem ========== +CONFIG_INPUT=y +CONFIG_INPUT_EVENT_DUMP=n +# Synchronous mode: callbacks run in reporting context (GPIO ISR / syswq) +# instead of a dedicated 1KB input thread. Saves a thread stack. +CONFIG_INPUT_MODE_SYNCHRONOUS=y + +# ========== Power Management ========== +CONFIG_POWEROFF=y + +# ========== LoRa Power Saving ========== +# RX duty cycle enabled by default for battery-powered devices +CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE=y + +# ========== ZephCore Log Levels ========== +# Defaults are INF for all modules (Kconfig default — no explicit =y needed). +# Override per-module on command line if needed: +# -DCONFIG_ZEPHCORE_BLE_LOG_LEVEL_DBG=y (BLE adapter only) +# -DCONFIG_ZEPHCORE_MAIN_LOG_LEVEL_DBG=y (mesh, companion, basechat, utils) diff --git a/zephcore/boards/esp32/xiao_esp32c3/board.conf b/zephcore/boards/esp32/xiao_esp32c3/board.conf new file mode 100644 index 0000000..a1ce088 --- /dev/null +++ b/zephcore/boards/esp32/xiao_esp32c3/board.conf @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: Apache-2.0 +# Seeed XIAO ESP32-C3 + SX1262 LoRa +# +# Hardware: +# - ESP32-C3 (RISC-V, 4MB flash, BLE 5.0) +# - SX1262 LoRa on SPI2: NSS=D4(GPIO6), DIO1=D1(GPIO3), BUSY=D3(GPIO5), +# RESET=D2(GPIO4), RXEN=D5(GPIO7) +# - TCXO 1.8V, DIO2 as TX enable, RX boosted +# - Battery ADC on D0 (GPIO2) — if wired +# - USB-Serial/JTAG for console + +# Board identification +CONFIG_ZEPHCORE_BOARD_NAME="XIAO ESP32-C3" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="XIAO ESP32-C3" + +# Radio type +CONFIG_ZEPHCORE_RADIO_NATIVE=y + +# Flash size (XIAO ESP32-C3 has 4MB, override Zephyr default 2MB) +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y diff --git a/zephcore/boards/esp32/xiao_esp32c3/board.overlay b/zephcore/boards/esp32/xiao_esp32c3/board.overlay new file mode 100644 index 0000000..532123c --- /dev/null +++ b/zephcore/boards/esp32/xiao_esp32c3/board.overlay @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * XIAO ESP32-C3 + SX1262 LoRa + * + * Pin mapping (from Arduino MeshCore variants/xiao_c3): + * - D4 (GPIO6) = NSS - D8 (GPIO8) = SPI SCK + * - D1 (GPIO3) = DIO1 - D9 (GPIO9) = SPI MISO + * - D3 (GPIO5) = BUSY - D10 (GPIO10) = SPI MOSI + * - D2 (GPIO4) = RESET + * - D5 (GPIO7) = RXEN + * - D0 (GPIO2) = VBAT (optional) + * + * Console: USB-Serial/JTAG (usb_serial) + */ + +/ { + aliases { + lora0 = &lora; + }; + + chosen { + zephyr,console = &usb_serial; + zephyr,shell-uart = &usb_serial; + zephyr,settings-partition = &storage_partition; + }; +}; + +/* SX1262 on SPI2 (XIAO SPI bus — SCK/MISO/MOSI already in pinctrl) */ +&spi2 { + cs-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>; /* D4 = GPIO6 NSS */ + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <8000000>; + + /* LoRa control pins */ + reset-gpios = <&gpio0 4 GPIO_ACTIVE_LOW>; /* D2 = GPIO4 */ + busy-gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>; /* D3 = GPIO5 */ + dio1-gpios = <&gpio0 3 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* D1 = GPIO3 */ + + /* RF switch: RXEN for RX, DIO2 drives TX enable */ + rx-enable-gpios = <&gpio0 7 GPIO_ACTIVE_HIGH>; /* D5 = GPIO7 RXEN */ + dio2-tx-enable; + + /* TCXO 1.8V via DIO3 (SX126X_DIO3_TCXO_1V8 = 0x02) */ + dio3-tcxo-voltage = <2>; + tcxo-power-startup-delay-ms = <10>; + + /* RX boosted mode for better sensitivity */ + rx-boosted; + }; +}; + +/* + * Flash partitions — repartition default ESP32 layout. + * Default storage_partition is 192KB at 0x3B0000. + * Split into: NVS 8KB (BLE bonds) + LittleFS 184KB (identity, contacts, etc.) + */ +/delete-node/ &storage_partition; + +&flash0 { + partitions { + /* NVS 8KB for BLE bonds + settings */ + storage_partition: partition@3b0000 { + label = "storage"; + reg = <0x3B0000 0x2000>; + }; + + /* LittleFS 184KB for DataStore */ + lfs_partition: partition@3b2000 { + label = "lfs"; + reg = <0x3B2000 0x2E000>; + }; + }; +}; + +/* LittleFS auto-mount — standard /lfs mount point */ +#include "../../common/filesystem.dtsi" diff --git a/zephcore/boards/esp32/xiao_esp32c6/board.conf b/zephcore/boards/esp32/xiao_esp32c6/board.conf new file mode 100644 index 0000000..30473e8 --- /dev/null +++ b/zephcore/boards/esp32/xiao_esp32c6/board.conf @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: Apache-2.0 +# Seeed XIAO ESP32-C6 + SX1262 LoRa +# +# Hardware: +# - ESP32-C6 (RISC-V, 4MB flash, BLE 5.0, Wi-Fi 6, 802.15.4) +# - SX1262 LoRa on SPI2: NSS=D4(GPIO22), DIO1=D1(GPIO1), BUSY=D3(GPIO21), +# RESET=D2(GPIO2), RXEN=D5(GPIO23) +# - TCXO 1.8V, DIO2 as TX enable, RX boosted +# - Yellow LED on GPIO15 +# - USB-Serial/JTAG for console + +# Board identification +CONFIG_ZEPHCORE_BOARD_NAME="XIAO ESP32-C6" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="XIAO ESP32-C6" + +# Radio type +CONFIG_ZEPHCORE_RADIO_NATIVE=y + +# Flash size (XIAO ESP32-C6 has 4MB, override Zephyr default 2MB) +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y diff --git a/zephcore/boards/esp32/xiao_esp32c6/board.overlay b/zephcore/boards/esp32/xiao_esp32c6/board.overlay new file mode 100644 index 0000000..026f14b --- /dev/null +++ b/zephcore/boards/esp32/xiao_esp32c6/board.overlay @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * XIAO ESP32-C6 + SX1262 LoRa + * + * Pin mapping (from Arduino MeshCore variants/xiao_c6): + * - D4 (GPIO22) = NSS - D8 (GPIO19) = SPI SCK + * - D1 (GPIO1) = DIO1 - D9 (GPIO20) = SPI MISO + * - D3 (GPIO21) = BUSY - D10 (GPIO18) = SPI MOSI + * - D2 (GPIO2) = RESET + * - D5 (GPIO23) = RXEN + * - GPIO15 = Yellow LED (TX indicator) + * + * Console: USB-Serial/JTAG (usb_serial) + */ + +/ { + aliases { + lora0 = &lora; + }; + + chosen { + zephyr,console = &usb_serial; + zephyr,shell-uart = &usb_serial; + zephyr,settings-partition = &storage_partition; + }; +}; + +/* SX1262 on SPI2 (XIAO SPI bus — SCK/MISO/MOSI already in pinctrl) */ +&spi2 { + cs-gpios = <&gpio0 22 GPIO_ACTIVE_LOW>; /* D4 = GPIO22 NSS */ + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <8000000>; + + /* LoRa control pins */ + reset-gpios = <&gpio0 2 GPIO_ACTIVE_LOW>; /* D2 = GPIO2 */ + busy-gpios = <&gpio0 21 GPIO_ACTIVE_HIGH>; /* D3 = GPIO21 */ + dio1-gpios = <&gpio0 1 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* D1 = GPIO1 */ + + /* RF switch: RXEN for RX, DIO2 drives TX enable */ + rx-enable-gpios = <&gpio0 23 GPIO_ACTIVE_HIGH>; /* D5 = GPIO23 RXEN */ + dio2-tx-enable; + + /* TCXO 1.8V via DIO3 (SX126X_DIO3_TCXO_1V8 = 0x02) */ + dio3-tcxo-voltage = <2>; + tcxo-power-startup-delay-ms = <10>; + + /* RX boosted mode for better sensitivity */ + rx-boosted; + }; +}; + +/* + * Flash partitions — repartition default ESP32 layout. + * Default storage_partition is 192KB at 0x3B0000. + * Split into: NVS 8KB (BLE bonds) + LittleFS 184KB (identity, contacts, etc.) + */ +/delete-node/ &storage_partition; + +&flash0 { + partitions { + /* NVS 8KB for BLE bonds + settings */ + storage_partition: partition@3b0000 { + label = "storage"; + reg = <0x3B0000 0x2000>; + }; + + /* LittleFS 184KB for DataStore */ + lfs_partition: partition@3b2000 { + label = "lfs"; + reg = <0x3B2000 0x2E000>; + }; + }; +}; + +/* LittleFS auto-mount — standard /lfs mount point */ +#include "../../common/filesystem.dtsi" diff --git a/zephcore/boards/example_board/README.md b/zephcore/boards/example_board/README.md new file mode 100644 index 0000000..2a9292a --- /dev/null +++ b/zephcore/boards/example_board/README.md @@ -0,0 +1,95 @@ +ZephCore — Adding a New Board +============================= + +There are TWO ways to add a board, depending on whether Zephyr +already has a board definition for your hardware. + + +PATTERN 1: Existing Zephyr Board (overlay only) +------------------------------------------------ + +Use this when your board already exists in Zephyr's tree. +You only need TWO files: board.conf + board.overlay + +Examples: XIAO nRF54L15, XIAO MG24, XIAO ESP32-C3, RAK4631 + +Directory structure: + zephcore/boards/// + board.conf — Kconfig (name, radio type) + board.overlay — DT overlay (LoRa SPI, partitions, peripherals) + +Steps: + 1. Create directory: boards/// + Platform folders: nrf52840, nrf54l, mg24, esp32 + 2. Copy board.conf and board.overlay from THIS directory + 3. Uncomment the sections matching your platform + 4. Fill in YOUR pin numbers and partition layout + 5. Add board detection to CMakeLists.txt (~line 60-75): + Add BOARD MATCHES "your_board" to the correct platform line + 6. Build and iterate! + +Build commands: + nRF52840: west build -b zephcore --pristine + nRF54L15: west build -b /nrf54l15/cpuapp zephcore --pristine --no-sysbuild + MG24: west blobs fetch hal_silabs && west build -b zephcore --pristine + ESP32-C3: west build -b zephcore --pristine + + +PATTERN 2: Fully Custom Board (new DTS) +----------------------------------------- + +Use this when your board does NOT exist in Zephyr's tree. +You need a full board definition: .dts, pinctrl, Kconfig, etc. + +Examples: Ikoka Nano 30dBm (custom nRF52840 design) + +Look at an existing custom board as a template: + zephcore/boards/nrf52840/ikoka_nano_30dbm/ + +A full custom board includes: + board.conf — Kconfig (name, radio, overrides) + board.overlay — DT overlay (usually empty if DTS is complete) + _.dts — Full device tree + _-pinctrl.dtsi — Pin control definitions + board.yml — Board metadata (name, arch, SoC) + Kconfig. — SoC selection + __defconfig — Minimal defconfig + board.cmake — Flash runner config + + +Config Inheritance +------------------ + + prj.conf Always loaded first + | + zephcore_common.conf BLE, storage, input, LoRa, crypto, sensors + | + _common.conf Platform-specific overrides only + | nrf52_common.conf — UF2, USB CDC, DLE, GNSS, RTT + | nrf54l_common.conf — DLE, RTT + | mg24_common.conf — SiLabs blob stacks, heap + | esp32_common.conf — Espressif blob stacks, heap + | + board.conf Board name, radio type, board-specific + +DO NOT duplicate settings from parent configs in board.conf! + + +Quick Reference: Wio-SX1262 XIAO Pin Mapping +--------------------------------------------- + +All XIAO boards use the same D-pin assignment for Wio-SX1262: + + Signal │ XIAO Pin │ nRF52840 │ nRF54L15 │ MG24 │ ESP32-C3 + ───────│──────────│───────────│───────────│───────────│───────── + DIO1 │ D1 │ P0.03 │ P1.05 │ PC01 │ GPIO3 + RESET │ D2 │ P0.28 │ P1.06 │ PC02 │ GPIO4 + BUSY │ D3 │ P0.05 │ P1.07 │ PC03 │ GPIO5 + NSS │ D4 │ P0.04 │ P1.10 │ PC04 │ GPIO6 + RXEN │ D5 │ P0.29 │ P1.11 │ PC05 │ GPIO7 + SCK │ D8 │ P1.13 │ P2.01 │ PA03 │ GPIO8 + MISO │ D9 │ P1.14 │ P2.04 │ PA04 │ GPIO9 + MOSI │ D10 │ P1.15 │ P2.02 │ PA05 │ GPIO10 + +Note: nRF52840 D-pin mapping varies by board (XIAO nRF52840 shown). +RAK4631 has SX1262 integrated — different pinout entirely. diff --git a/zephcore/boards/example_board/board.conf b/zephcore/boards/example_board/board.conf new file mode 100644 index 0000000..bd6a895 --- /dev/null +++ b/zephcore/boards/example_board/board.conf @@ -0,0 +1,98 @@ +# ============================================================================ +# ZephCore — Universal board.conf Template +# ============================================================================ +# +# This file is ONLY for board-specific features. All common config (BLE stack, +# storage, input, LoRa, sensors, crypto) is in zephcore_common.conf. +# Platform-specific overrides are in _common.conf. +# +# Config chain (later files override earlier): +# prj.conf → zephcore_common.conf → _common.conf → THIS FILE +# +# Platform configs are auto-selected in CMakeLists.txt based on BOARD name: +# nrf52 → nrf52_common.conf (UF2, USB CDC, DLE, GNSS, RTT) +# nrf54l → nrf54l_common.conf (DLE, RTT) +# mg24 → mg24_common.conf (SiLabs blob stacks, larger heap) +# esp32 → esp32_common.conf (Espressif blob stacks, larger heap) +# +# DO NOT duplicate settings from the common configs! +# +# IMPORTANT: Register your board in CMakeLists.txt! +# Add "OR BOARD MATCHES \"your_board\"" to the correct platform detection +# line (~line 60-75) so the right _common.conf is auto-included. +# +# ============================================================================ + +# ========== REQUIRED: Board Identity ========== +# Name shown on phone app and OLED top bar +CONFIG_ZEPHCORE_BOARD_NAME="My Board" + +# BLE Device Information Service model name (shown in phone settings) +CONFIG_BT_DIS_MODEL_NUMBER_STR="My Board v1" + +# ========== REQUIRED: Radio Type ========== +# Pick ONE based on your LoRa chip. +CONFIG_ZEPHCORE_RADIO_NATIVE=y # Zephyr native LoRa driver (SX126x, SX127x, LLCC68, STM32WL) +# CONFIG_ZEPHCORE_RADIO_LR1110=y # Semtech LR11xx custom driver (LR1110, LR1120, LR1121) + +# ========== OPTIONAL: SPI for LoRa ========== +# Only needed if LoRa chip is NOT on the Zephyr lora driver (i.e. LR11xx). +# Native driver radios use Zephyr's built-in lora driver and do NOT need this. +# CONFIG_SPI=y + +# ========== OPTIONAL: QSPI External Flash (nRF52840 only) ========== +# Uncomment if your board has QSPI flash for expanded storage. +# This gives 100 advert blob slots instead of 20. +# Only available on nRF52840 — other MCUs don't have QSPI peripheral. +# +# WARNING: If your board does NOT have a QSPI flash chip, you MUST leave +# this disabled (=n) AND disable &qspi in your overlay. The Nordic QSPI +# driver hangs forever at boot (k_sem_take K_FOREVER) if no chip responds, +# bricking the device with no USB/BLE/logs. The DataStore code handles +# missing QSPI gracefully — the driver does not. +# +# Also add &qspi { status = "disabled"; }; in your overlay and do NOT +# include qspi-ext.dtsi. See boards/nrf52840/rak4631/ for an example. +CONFIG_NORDIC_QSPI_NOR=n + +# ========== OPTIONAL: Display ========== +# Display subsystem is enabled universally in zephcore_common.conf. +# Display DRIVERS auto-select from the DT "compatible" string. +# No board.conf changes needed — just add the display node to your overlay. +# +# To add a display: +# 1. Add the display node to your board.overlay (see examples there) +# 2. Set zephyr,display = &your_display; in the "chosen" block +# 3. That's it — driver, CFB, and UI all auto-enable + +# ========== OPTIONAL: Buzzer ========== +# Uncomment if your board has a piezo buzzer on a PWM pin. +# CONFIG_PWM=y + +# ========== OPTIONAL: Platform-Specific Overrides ========== +# +# --- nRF52840 --- +# CRITICAL: SoftDevice version determines the app start address! +# Check your board's Arduino platformio.ini for which linker script it uses: +# nrf52840_s140_v6*.ld → s140 v6.1.1, app@0x26000, FWID=0x00B6 +# nrf52840_s140_v7*.ld → s140 v7.3.0, app@0x27000, FWID=0x0100 +# Your board.overlay partition table MUST match (see Section 4 in board.overlay). +# Wrong address = bootloop / bricked device with no USB/BLE/logs. +# CONFIG_ZEPHCORE_SD_FWID=0x00B6 # SoftDevice firmware ID (s140 v6.1.1) +# CONFIG_ZEPHCORE_SD_FWID=0x0100 # SoftDevice firmware ID (s140 v7.3.0) +# +# --- MG24 (Silicon Labs) --- +# Requires: west blobs fetch hal_silabs (before first build) +# No additional board.conf options typically needed. +# +# --- nRF54L15 --- +# No additional board.conf options typically needed. +# Build with: west build -b /nrf54l15/cpuapp zephcore --no-sysbuild +# +# --- ESP32 --- +# CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y # If your board has 4MB flash (default 2MB) + +# ========== OPTIONAL: Disable RX Duty Cycle ========== +# Inherited from platform common config as enabled (power savings). +# Override here if your board is wall-powered. +# CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE=n diff --git a/zephcore/boards/example_board/board.overlay b/zephcore/boards/example_board/board.overlay new file mode 100644 index 0000000..c378112 --- /dev/null +++ b/zephcore/boards/example_board/board.overlay @@ -0,0 +1,720 @@ +/* + * ============================================================================ + * ZephCore — Universal board.overlay Template + * ============================================================================ + * + * This overlay adds ZephCore-specific nodes ON TOP of an existing Zephyr + * board definition. Use this when your board already exists in Zephyr's tree + * (e.g., xiao_nrf54l15, xiao_mg24, xiao_esp32c3, rak4631). + * + * If you're creating a FULLY CUSTOM board (no upstream Zephyr DTS), look at + * boards/nrf52840/ikoka_nano_30dbm/ as a reference — it includes a complete + * .dts, pinctrl, Kconfig, and board.cmake. + * + * ┌──────────────────────────────────────────────────────────────────────┐ + * │ WHAT THIS FILE DOES │ + * │ │ + * │ 1. Adds LoRa radio on the board's SPI bus │ + * │ 2. Sets flash partitions for NVS + LittleFS │ + * │ 3. Optionally disables conflicting peripherals │ + * │ 4. Adds aliases and chosen nodes for ZephCore │ + * │ │ + * │ PLATFORM DIFFERENCES (see each section): │ + * │ │ + * │ │ Feature │ nRF52840 │ nRF54L15 │ MG24 │ ESP32 │ │ + * │ │──────────────│────────────│───────────│───────────│───────────│ │ + * │ │ SPI bus │ &spi1 │ &spi00 │ &eusart0 │ &spi2 │ │ + * │ │ GPIO prefix │ &gpio0/1 │ &gpio1/2 │ &gpioa-d │ &gpio0 │ │ + * │ │ Flash node │ &flash0 │ cpuapp_rram│ &flash0 │ &flash0 │ │ + * │ │ Erase block │ 4KB │ 4KB │ 8KB │ 4KB │ │ + * │ │ Console │ USB CDC │ &uart20* │ &usart0* │ usb_serial│ │ + * │ │ Pinctrl │ NRF_PSEL() │ NRF_PSEL()│ in DTS │ in DTS │ │ + * │ │ Boot region │ SoftDevice │ MCUboot │ MCUboot │ image hdr │ │ + * │ │ │ + * │ * = already set in Zephyr's board DTS, no override needed │ + * └──────────────────────────────────────────────────────────────────────┘ + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +/* ================================================================== + * SECTION 1: ALIASES AND CHOSEN (REQUIRED) + * + * Every board MUST define: + * lora0 alias — points to LoRa radio node (required) + * settings-partition — NVS for BLE bonds (required) + * + * Console is ONLY needed if the board doesn't already set it: + * nRF52840: needs zephyr,console = &cdc_acm_uart (USB CDC) + * nRF54L15: already set to &uart20 in board DTS — skip + * MG24: already set to &usart0 in board DTS — skip + * ESP32: needs zephyr,console = &usb_serial + * ================================================================== */ +/ { + aliases { + lora0 = &lora; + + /* Optional aliases (uncomment if your board has these): + * led0 = &user_led; heartbeat LED + * sw0 = &user_button; primary button + * buzzer = &buzzer; PWM buzzer + * gps-enable = &gps_enable; GPS power control + */ + }; + + chosen { + zephyr,settings-partition = &storage_partition; + + /* --- Console (only if board DTS doesn't already set it) --- + * + * nRF52840 (USB CDC): + * zephyr,console = &cdc_acm_uart; + * zephyr,shell-uart = &cdc_acm_uart; + * zephyr,code-partition = &slot0_partition; + * + * ESP32 (USB Serial/JTAG): + * zephyr,console = &usb_serial; + * zephyr,shell-uart = &usb_serial; + * + * nRF54L15: skip (uart20 already set) + * MG24: skip (usart0 already set) + */ + + /* --- Display (uncomment if board has OLED/LCD) --- + * zephyr,display = &oled; + */ + }; +}; + + +/* ================================================================== + * SECTION 2: PERIPHERAL CONFLICTS (platform-specific) + * + * Many XIAO boards have I2C default pins that conflict with LoRa + * control signals. You MUST disable the conflicting peripheral. + * + * Check your board's DTS for I2C pin assignments and compare with + * your LoRa wiring. Common conflicts: + * + * Board │ Conflict │ Fix + * ───────────────│──────────────────────│──────────────────── + * XIAO nRF54L15 │ i2c22 on P1.10/P1.11│ Disable &i2c22 + * XIAO MG24 │ i2c0 on PC4/PC5 │ Disable &i2c0 + * XIAO ESP32-C3 │ (none) │ No action needed + * XIAO nRF52840 │ (none) │ No action needed + * RAK4631 │ (none) │ No action needed + * ================================================================== */ + +/* --- nRF54L15 example: disable I2C that conflicts with NSS/RXEN --- + * + * &i2c22 { + * status = "disabled"; + * }; + */ + +/* --- MG24 example: disable I2C that conflicts with NSS/RXEN --- + * + * &i2c0 { + * status = "disabled"; + * }; + */ + + +/* ================================================================== + * SECTION 3: LORA RADIO ON SPI (REQUIRED) + * + * This is the most important section — without it, no mesh. + * + * Pick your SPI bus based on platform: + * nRF52840: &spi1 (or &spi2, &spi3 — avoid &spi0 if using I2C0) + * nRF54L15: &spi00 (XIAO SPI bus, pinctrl already set) + * MG24: &eusart0 (XIAO SPI bus, pinctrl already set) + * ESP32-C3: &spi2 + * ESP32-C6: &spi2 + * + * GPIO syntax differs per MCU: + * nRF52840: <&gpio0 PIN FLAGS> or <&gpio1 PIN FLAGS> + * nRF54L15: <&gpio1 PIN FLAGS> or <&gpio2 PIN FLAGS> + * MG24: <&gpioa PIN FLAGS>, <&gpioc PIN FLAGS>, etc. + * ESP32: <&gpio0 PIN FLAGS> + * + * SX1262 pin connections: + * cs-gpios — SPI chip select (NSS), ACTIVE_LOW + * reset-gpios — Radio reset, ACTIVE_LOW + * busy-gpios — Busy indicator, ACTIVE_HIGH + * dio1-gpios — Interrupt (RX/TX done), ACTIVE_HIGH with PULL_DOWN + * + * RF switch (pick what your module needs): + * dio2-tx-enable — DIO2 controls TX (most modules) + * rx-enable-gpios — Separate RXEN pin (Wio-SX1262 needs both) + * + * TCXO (if your module has one): + * dio3-tcxo-voltage — DIO3 provides TCXO power + * Values: 0=1.6V, 1=1.7V, 2=1.8V, 3=2.2V, 4=2.4V, 5=2.7V, 6=3.0V, 7=3.3V + * Or use named constants from sx126x.h: + * SX126X_DIO3_TCXO_1V6 .. SX126X_DIO3_TCXO_3V3 + * + * rx-boosted — Better RX sensitivity (slight power increase) + * + * CHANGE ALL PINS to match your board! + * + * ================================================================== */ + +/* ┌─────────────────────────────────────────┐ + * │ EXAMPLE A: nRF52840 — custom SPI pins │ + * │ (pinctrl defined in separate .dtsi) │ + * └─────────────────────────────────────────┘ + * + * &spi1 { + * compatible = "nordic,nrf-spim"; + * status = "okay"; + * cs-gpios = <&gpio0 24 GPIO_ACTIVE_LOW>; + * pinctrl-0 = <&spi1_default>; + * pinctrl-1 = <&spi1_sleep>; + * pinctrl-names = "default", "sleep"; + * + * lora: lora@0 { + * compatible = "semtech,sx1262"; + * reg = <0>; + * spi-max-frequency = <8000000>; + * reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>; + * busy-gpios = <&gpio1 10 GPIO_ACTIVE_HIGH>; + * dio1-gpios = <&gpio0 7 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + * dio2-tx-enable; + * dio3-tcxo-voltage = ; + * tcxo-power-startup-delay-ms = <5>; + * }; + * }; + */ + +/* ┌─────────────────────────────────────────┐ + * │ EXAMPLE B: nRF54L15 (XIAO) — spi00 │ + * │ SCK/MISO/MOSI already in board pinctrl │ + * └─────────────────────────────────────────┘ + * + * &spi00 { + * cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>; + * + * lora: lora@0 { + * compatible = "semtech,sx1262"; + * reg = <0>; + * spi-max-frequency = <8000000>; + * reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>; + * busy-gpios = <&gpio1 7 GPIO_ACTIVE_HIGH>; + * dio1-gpios = <&gpio1 5 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + * rx-enable-gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>; + * dio2-tx-enable; + * dio3-tcxo-voltage = <6>; + * tcxo-power-startup-delay-ms = <10>; + * rx-boosted; + * }; + * }; + */ + +/* ┌─────────────────────────────────────────┐ + * │ EXAMPLE C: MG24 (XIAO) — eusart0 │ + * │ SCK/MISO/MOSI already in board pinctrl │ + * │ Default CS is PC7 — override to PC4 │ + * └─────────────────────────────────────────┘ + * + * &eusart0 { + * status = "okay"; + * cs-gpios = <&gpioc 4 GPIO_ACTIVE_LOW>; + * + * lora: lora@0 { + * compatible = "semtech,sx1262"; + * reg = <0>; + * spi-max-frequency = <8000000>; + * reset-gpios = <&gpioc 2 GPIO_ACTIVE_LOW>; + * busy-gpios = <&gpioc 3 GPIO_ACTIVE_HIGH>; + * dio1-gpios = <&gpioc 1 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + * rx-enable-gpios = <&gpioc 5 GPIO_ACTIVE_HIGH>; + * dio2-tx-enable; + * dio3-tcxo-voltage = <6>; + * tcxo-power-startup-delay-ms = <10>; + * rx-boosted; + * }; + * }; + */ + +/* ┌─────────────────────────────────────────┐ + * │ EXAMPLE D: ESP32-C3/C6 — spi2 │ + * │ SCK/MISO/MOSI already in board pinctrl │ + * └─────────────────────────────────────────┘ + * + * &spi2 { + * cs-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>; + * + * lora: lora@0 { + * compatible = "semtech,sx1262"; + * reg = <0>; + * spi-max-frequency = <8000000>; + * reset-gpios = <&gpio0 4 GPIO_ACTIVE_LOW>; + * busy-gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>; + * dio1-gpios = <&gpio0 3 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + * rx-enable-gpios = <&gpio0 7 GPIO_ACTIVE_HIGH>; + * dio2-tx-enable; + * dio3-tcxo-voltage = <6>; + * tcxo-power-startup-delay-ms = <10>; + * rx-boosted; + * }; + * }; + */ + + +/* ================================================================== + * SECTION 4: FLASH PARTITIONS (REQUIRED) + * + * ZephCore needs three partitions: + * slot0_partition — Application code + * storage_partition — NVS for BLE bonds + settings + * lfs_partition — LittleFS for identity, contacts, channels + * + * Most boards also have a boot region (SoftDevice, MCUboot, or image + * header) that should NOT be touched. + * + * CRITICAL: Erase block size determines minimum NVS size: + * 4KB erase (nRF52, nRF54L, ESP32): 2 × 4KB = 8KB minimum + * 8KB erase (MG24): 2 × 8KB = 16KB minimum + * + * General approach: + * 1. Delete default partitions (slot0, slot1, storage) + * 2. Expand slot0 into slot1 space (no DFU for companion device) + * 3. Add NVS + LittleFS at the end + * + * ================================================================== */ + +/* ┌─────────────────────────────────────────────────────────────────────┐ + * │ EXAMPLE A: nRF52840 (1MB flash, 4KB erase, SoftDevice + UF2) │ + * │ │ + * │ CRITICAL: You MUST match the SoftDevice version that your board's │ + * │ bootloader was flashed with. Getting this wrong means the app is │ + * │ placed at the wrong address and the board will bootloop / brick. │ + * │ │ + * │ Check the Arduino variant's platformio.ini for your board: │ + * │ s140_v6 linker script → app starts at 0x26000 (SD is 152KB) │ + * │ s140_v7 linker script → app starts at 0x27000 (SD is 156KB) │ + * │ │ + * │ The bootloader address also varies. Parse the bootloader hex to │ + * │ find it (highest address region), or read UICR 0x10001014 via │ + * │ J-Link. Common values: 0xF4000 (v6 boards), 0xED000 (v7 boards). │ + * │ │ + * │ s140 v6 boards (app@0x26000): │ + * │ RAK4631, heltec_t114, heltec_mesh_solar, lilygo_techo, │ + * │ lilygo_techo_lite, mesh_pocket, meshtiny, nano_g2_ultra, │ + * │ promicro, rak3401, rak_wismesh_tag, keepteen_lt1, │ + * │ thinknode_m1/m3/m6 │ + * │ │ + * │ s140 v7 boards (app@0x27000): │ + * │ xiao_nrf52, wio_wm1110, wio-tracker-l1, wio-tracker-l1-eink, │ + * │ sensecap_solar, t1000-e, minewsemi_me25ls01, │ + * │ ikoka_nano_nrf, ikoka_stick_nrf, ikoka_handheld_nrf │ + * │ │ + * │ Example: s140 v6 (RAK4631, bootloader@0xF4000): │ + * │ 0x00000 - 0x26000 (152KB) SoftDevice s140 v6.1.1 (reserved) │ + * │ 0x26000 - 0xD4000 (696KB) Application │ + * │ 0xD4000 - 0xD6000 (8KB) NVS (2 × 4KB) │ + * │ 0xD6000 - 0xF4000 (120KB) LittleFS │ + * │ 0xF4000 - 0x100000 (48KB) UF2 bootloader (reserved) │ + * │ │ + * │ Example: s140 v7 (xiao_nrf52, bootloader@0xED000): │ + * │ 0x00000 - 0x27000 (156KB) SoftDevice s140 v7.3.0 (reserved) │ + * │ 0x27000 - 0xD4000 (692KB) Application │ + * │ 0xD4000 - 0xD6000 (8KB) NVS (2 × 4KB) │ + * │ 0xD6000 - 0xED000 (92KB) LittleFS │ + * │ 0xED000 - 0x100000 (76KB) UF2 bootloader (reserved) │ + * └─────────────────────────────────────────────────────────────────────┘ + * + * /delete-node/ &boot_partition; + * /delete-node/ &slot0_partition; + * /delete-node/ &slot1_partition; + * /delete-node/ &storage_partition; + * + * --- s140 v6 variant (app@0x26000) --- + * + * &flash0 { + * partitions { + * compatible = "fixed-partitions"; + * #address-cells = <1>; + * #size-cells = <1>; + * + * boot_partition: partition@0 { + * label = "softdevice"; + * read-only; + * reg = <0x00000 0x26000>; + * }; + * slot0_partition: partition@26000 { + * label = "Application"; + * reg = <0x26000 0xAE000>; + * }; + * storage_partition: partition@d4000 { + * label = "storage"; + * reg = <0xD4000 0x2000>; + * }; + * lfs_partition: partition@d6000 { + * label = "lfs"; + * reg = <0xD6000 0x1E000>; + * }; + * uf2_partition: partition@f4000 { + * label = "UF2"; + * read-only; + * reg = <0xF4000 0xC000>; + * }; + * }; + * }; + * + * --- s140 v7 variant (app@0x27000) --- + * + * &flash0 { + * partitions { + * compatible = "fixed-partitions"; + * #address-cells = <1>; + * #size-cells = <1>; + * + * boot_partition: partition@0 { + * label = "softdevice"; + * read-only; + * reg = <0x00000 0x27000>; + * }; + * slot0_partition: partition@27000 { + * label = "Application"; + * reg = <0x27000 0xAD000>; + * }; + * storage_partition: partition@d4000 { + * label = "storage"; + * reg = <0xD4000 0x2000>; + * }; + * lfs_partition: partition@d6000 { + * label = "lfs"; + * reg = <0xD6000 0x17000>; + * }; + * uf2_partition: partition@ed000 { + * label = "UF2"; + * read-only; + * reg = <0xED000 0x13000>; + * }; + * }; + * }; + */ + +/* ┌─────────────────────────────────────────────────────────────────────┐ + * │ EXAMPLE B: nRF54L15 (1428KB RRAM, 4KB erase, MCUboot reserved) │ + * │ │ + * │ 0x00000 - 0x10000 (64KB) MCUboot (reserved) │ + * │ 0x10000 - 0x14E000 (1272KB) Application │ + * │ 0x14E000 - 0x150000 (8KB) NVS (2 × 4KB) │ + * │ 0x150000 - 0x165000 (84KB) LittleFS │ + * │ │ + * │ NOTE: nRF54L uses RRAM not flash — node is &cpuapp_rram │ + * └─────────────────────────────────────────────────────────────────────┘ + * + * /delete-node/ &slot0_partition; + * /delete-node/ &slot1_partition; + * /delete-node/ &storage_partition; + * + * &cpuapp_rram { + * partitions { + * slot0_partition: partition@10000 { + * label = "image-0"; + * reg = <0x10000 0x13E000>; + * }; + * storage_partition: partition@14e000 { + * label = "storage"; + * reg = <0x14E000 0x2000>; + * }; + * lfs_partition: partition@150000 { + * label = "lfs"; + * reg = <0x150000 0x15000>; + * }; + * }; + * }; + */ + +/* ┌─────────────────────────────────────────────────────────────────────┐ + * │ EXAMPLE C: MG24 (1536KB flash, 8KB erase, MCUboot reserved) │ + * │ │ + * │ 0x00000 - 0x0C000 (48KB) MCUboot (reserved) │ + * │ 0x0C000 - 0x15C000 (1344KB) Application │ + * │ 0x15C000 - 0x160000 (16KB) NVS (2 × 8KB — MG24 has 8KB erase!) │ + * │ 0x160000 - 0x180000 (128KB) LittleFS │ + * └─────────────────────────────────────────────────────────────────────┘ + * + * /delete-node/ &slot0_partition; + * /delete-node/ &slot1_partition; + * /delete-node/ &storage_partition; + * + * &flash0 { + * partitions { + * slot0_partition: partition@c000 { + * label = "image-0"; + * reg = <0x0C000 0x150000>; + * }; + * storage_partition: partition@15c000 { + * label = "storage"; + * reg = <0x15C000 0x4000>; + * }; + * lfs_partition: partition@160000 { + * label = "lfs"; + * reg = <0x160000 0x20000>; + * }; + * }; + * }; + */ + +/* ┌─────────────────────────────────────────────────────────────────────┐ + * │ EXAMPLE D: ESP32 (4MB flash, 4KB erase, simple boot at 0x0) │ + * │ │ + * │ Default layout already has large storage_partition at end. │ + * │ We just split it into NVS + LittleFS: │ + * │ │ + * │ 0x3B0000 - 0x3B2000 (8KB) NVS (2 × 4KB) │ + * │ 0x3B2000 - 0x3E0000 (184KB) LittleFS │ + * │ │ + * │ NOTE: Application partition is NOT changed — just storage split. │ + * └─────────────────────────────────────────────────────────────────────┘ + * + * /delete-node/ &storage_partition; + * + * &flash0 { + * partitions { + * storage_partition: partition@3b0000 { + * label = "storage"; + * reg = <0x3B0000 0x2000>; + * }; + * lfs_partition: partition@3b2000 { + * label = "lfs"; + * reg = <0x3B2000 0x2E000>; + * }; + * }; + * }; + */ + + +/* ================================================================== + * SECTION 5: LITTLEFS MOUNT (REQUIRED) + * + * Always include at the END of the overlay (after lfs_partition is + * defined). This provides the standard /lfs mount point. + * ================================================================== */ + +/* #include "../../common/filesystem.dtsi" */ + + +/* ================================================================== + * SECTION 5b: QSPI EXTERNAL FLASH (nRF52840 only, OPTIONAL) + * + * WARNING: The Nordic QSPI NOR driver hangs FOREVER at boot if no + * flash chip responds (k_sem_take K_FOREVER in nrf_qspi_nor.c). + * This bricks the device — no USB, no BLE, no logs. + * + * ONLY enable QSPI if your base board physically has a QSPI flash chip. + * If unsure, leave QSPI disabled. The DataStore falls back to internal + * flash gracefully (20 blob slots instead of 100). + * + * To enable QSPI: + * 1. Set CONFIG_NORDIC_QSPI_NOR=y in board.conf + * 2. Add the qspi_flash node below (uncommented) + * 3. Add #include "../../common/qspi-ext.dtsi" for /ext mount + * + * To disable QSPI (default, SAFE): + * 1. Set CONFIG_NORDIC_QSPI_NOR=n in board.conf + * 2. Add &qspi { status = "disabled"; }; to override upstream DTS + * (some boards like RAK4631 have &qspi enabled in upstream DTS) + * 3. Do NOT include qspi-ext.dtsi + * + * Boards WITH QSPI: RAK19007 base, RAK5005-O base + * Boards WITHOUT: RAK19003 mini base, most XIAO boards + * ================================================================== */ + +/* --- QSPI disabled (safe default) --- + * + * &qspi { + * status = "disabled"; + * }; + */ + +/* --- QSPI enabled (only if chip is physically present) --- + * + * &qspi { + * status = "okay"; + * pinctrl-0 = <&qspi_default>; + * pinctrl-1 = <&qspi_sleep>; + * pinctrl-names = "default", "sleep"; + * + * qspi_flash: qspi_flash@0 { + * compatible = "nordic,qspi-nor"; + * reg = <0>; + * writeoc = "pp4io"; + * readoc = "read4io"; + * sck-frequency = <16000000>; + * jedec-id = [85 60 15]; + * sfdp-bfp = [ + * e5 20 f1 ff ff ff ff 01 44 eb 08 6b 08 3b 04 bb + * ee ff ff ff ff ff 00 ff ff ff 00 ff 0c 20 0f 52 + * 10 d8 00 ff + * ]; + * size = <16777216>; + * has-dpd; + * t-enter-dpd = <3000>; + * t-exit-dpd = <30000>; + * quad-enable-requirements = "S2B1v1"; + * }; + * }; + * + * #include "../../common/qspi-ext.dtsi" + * + * &qspi_flash { + * partitions { + * compatible = "fixed-partitions"; + * #address-cells = <1>; + * #size-cells = <1>; + * qspi_storage_partition: partition@0 { + * label = "qspi_storage"; + * reg = <0x00000000 0x00200000>; + * }; + * }; + * }; + */ + + +/* ================================================================== + * SECTION 6: USB CDC (nRF52840 only) + * + * nRF52840 boards that use USB for console need this. + * Other platforms skip this entirely: + * nRF54L15: console via UART20 (SAMD11 bridge) + * MG24: console via USART0 + * ESP32: console via built-in USB-serial + * ================================================================== */ + +/* --- nRF52840 USB CDC --- + * + * zephyr_udc0: &usbd { + * compatible = "nordic,nrf-usbd"; + * status = "okay"; + * + * cdc_acm_uart: cdc_acm_uart { + * compatible = "zephyr,cdc-acm-uart"; + * }; + * }; + */ + + +/* ================================================================== + * SECTION 7: OPTIONAL PERIPHERALS + * + * These are NOT required for basic mesh operation but add features. + * See boards/nrf52840/ikoka_nano_30dbm/ for a fully annotated + * real-world example with all options explained. + * ================================================================== */ + +/* --- Battery ADC (nRF52840 only — other platforms don't have SAADC) --- + * + * / { + * zephyr,user { + * io-channels = <&adc 7>; + * vbat-mv-multiplier = <7200>; + * }; + * }; + * + * &adc { + * status = "okay"; + * #address-cells = <1>; + * #size-cells = <0>; + * channel@7 { + * reg = <7>; + * zephyr,gain = "ADC_GAIN_1_6"; + * zephyr,reference = "ADC_REF_INTERNAL"; + * zephyr,acquisition-time = ; + * zephyr,input-positive = ; + * zephyr,resolution = <12>; + * }; + * }; + */ + +/* --- LEDs (any platform) --- + * + * / { + * leds { + * compatible = "gpio-leds"; + * user_led: led_0 { + * gpios = <&gpio1 1 GPIO_ACTIVE_HIGH>; + * label = "User LED"; + * }; + * }; + * }; + */ + +/* --- Buttons with input filter chain (any platform) --- + * + * / { + * buttons: buttons { + * compatible = "gpio-keys"; + * user_button: button_0 { + * gpios = <&gpio0 8 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + * zephyr,code = ; + * }; + * }; + * + * user_btn_longpress { + * compatible = "zephyr,input-longpress"; + * input = <&buttons>; + * input-codes = ; + * short-codes = ; + * long-codes = ; + * long-delay-ms = <1000>; + * }; + * + * user_btn_multitap { + * compatible = "zephcore,input-multi-tap"; + * input-codes = ; + * tap-codes = ; + * tap-delay-ms = <400>; + * }; + * }; + */ + +/* --- GPS on UART (nRF52840 typically on &uart0) --- + * + * Supported: "luatos,air530z", "quectel,lc76g", "gnss-nmea-generic" + * + * &uart0 { + * current-speed = <9600>; + * gnss: gnss { + * compatible = "luatos,air530z"; + * on-off-gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>; + * }; + * }; + */ + +/* --- OLED Display on I2C (any platform with I2C) --- + * + * Supported: "sinowealth,sh1106", "solomon,ssd1306fb" + * Remember to set zephyr,display = &oled; in chosen block. + * + * &i2c0 { + * zephyr,concat-buf-size = <256>; + * oled: sh1106@3c { + * compatible = "sinowealth,sh1106"; + * reg = <0x3c>; + * width = <128>; + * height = <64>; + * segment-offset = <2>; + * page-offset = <0>; + * display-offset = <0>; + * multiplex-ratio = <63>; + * segment-remap; + * com-invdir; + * }; + * }; + */ + +/* --- I2C Environment Sensors (any platform with I2C) --- + * + * Auto-detected at runtime. Just include the common dtsi. + * + * &i2c0 { + * #include "../../common/sensors-i2c.dtsi" + * }; + */ diff --git a/zephcore/boards/mg24/xiao_mg24/board.conf b/zephcore/boards/mg24/xiao_mg24/board.conf new file mode 100644 index 0000000..9c1ac0d --- /dev/null +++ b/zephcore/boards/mg24/xiao_mg24/board.conf @@ -0,0 +1,12 @@ +# Seeed XIAO MG24 + Wio-SX1262 +# Board-specific configuration +# Requires: west blobs fetch hal_silabs (Silicon Labs BLE controller blob) + +# Board identification +CONFIG_ZEPHCORE_BOARD_NAME="XIAO MG24" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="XIAO MG24" + +# Radio type +CONFIG_ZEPHCORE_RADIO_NATIVE=y diff --git a/zephcore/boards/mg24/xiao_mg24/board.overlay b/zephcore/boards/mg24/xiao_mg24/board.overlay new file mode 100644 index 0000000..c1b696e --- /dev/null +++ b/zephcore/boards/mg24/xiao_mg24/board.overlay @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * XIAO MG24 + Wio-SX1262 LoRa + * + * Pin mapping (XIAO MG24 connector → Wio-SX1262): + * - D1 (PC01) = DIO1 - D8 (PA03) = SPI SCK (eusart0) + * - D2 (PC02) = RESET - D9 (PA04) = SPI MISO (eusart0) + * - D3 (PC03) = BUSY - D10 (PA05) = SPI MOSI (eusart0) + * - D4 (PC04) = NSS + * - D5 (PC05) = RXEN + * + * Console: USART0 (PA08 TX, PA09 RX) + * + * WARNING: Default I2C0 uses PC4/PC5 (D4/D5) — must be disabled for LoRa! + */ + +/ { + aliases { + lora0 = &lora; + }; + + chosen { + zephyr,settings-partition = &storage_partition; + }; +}; + +/* Disable I2C0 — default pins PC4(SCL)/PC5(SDA) conflict with NSS/RXEN */ +&i2c0 { + status = "disabled"; +}; + +/* SX1262 on eusart0 (XIAO SPI bus — SCK/MISO/MOSI already in pinctrl) */ +&eusart0 { + status = "okay"; + cs-gpios = <&gpioc 4 GPIO_ACTIVE_LOW>; /* D4 = PC04 NSS (override default PC07) */ + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <8000000>; + + /* LoRa control pins */ + reset-gpios = <&gpioc 2 GPIO_ACTIVE_LOW>; /* D2 = PC02 */ + busy-gpios = <&gpioc 3 GPIO_ACTIVE_HIGH>; /* D3 = PC03 */ + dio1-gpios = <&gpioc 1 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* D1 = PC01 */ + + /* RF switch: RXEN for RX, DIO2 drives TX enable */ + rx-enable-gpios = <&gpioc 5 GPIO_ACTIVE_HIGH>; /* D5 = PC05 RXEN */ + dio2-tx-enable; + + /* TCXO 1.8V via DIO3 (SX126X_DIO3_TCXO_1V8 = 0x02) */ + dio3-tcxo-voltage = <2>; + tcxo-power-startup-delay-ms = <10>; + + /* RX boosted mode for better sensitivity */ + rx-boosted; + }; +}; + +/* + * Flash partitions — repartition for ZephCore + * Total flash: 1536KB (0x180000), erase block = 8KB + * + * Default: boot(48KB) + slot0(736KB) + slot1(736KB) + storage(16KB) = 1536KB + * New layout (no MCUboot DFU for companion device): + * + * 0x00000 - 0x0C000 (48KB) MCUboot (reserved) + * 0x0C000 - 0x15C000 (1344KB) Application (slot0 — expanded into slot1 space) + * 0x15C000 - 0x160000 (16KB) NVS storage (BLE bonds) — 2 sectors × 8KB + * 0x160000 - 0x180000 (128KB) LittleFS (identity, contacts, channels) + */ +/delete-node/ &slot0_partition; +/delete-node/ &slot1_partition; +/delete-node/ &storage_partition; + +&flash0 { + partitions { + /* Application — expanded to use slot1 space */ + slot0_partition: partition@c000 { + label = "image-0"; + reg = <0x0C000 0x150000>; /* 1344KB */ + }; + + /* NVS 16KB for BLE bonds + settings (2 × 8KB sectors) */ + storage_partition: partition@15c000 { + label = "storage"; + reg = <0x15C000 0x4000>; + }; + + /* LittleFS 128KB for DataStore */ + lfs_partition: partition@160000 { + label = "lfs"; + reg = <0x160000 0x20000>; + }; + }; +}; + +/* LittleFS auto-mount — standard /lfs mount point */ +#include "../../common/filesystem.dtsi" diff --git a/zephcore/boards/nrf52840/ikoka_nano_30dbm/Kconfig.ikoka_nano_30dbm b/zephcore/boards/nrf52840/ikoka_nano_30dbm/Kconfig.ikoka_nano_30dbm new file mode 100644 index 0000000..a55ac50 --- /dev/null +++ b/zephcore/boards/nrf52840/ikoka_nano_30dbm/Kconfig.ikoka_nano_30dbm @@ -0,0 +1,5 @@ +# Ikoka Nano 30dBm board configuration +# DC/DC converter enabled via DTS: ®1 { regulator-initial-mode = ; } + +config BOARD_IKOKA_NANO_30DBM + select SOC_NRF52840_QIAA diff --git a/zephcore/boards/nrf52840/ikoka_nano_30dbm/board.conf b/zephcore/boards/nrf52840/ikoka_nano_30dbm/board.conf new file mode 100644 index 0000000..f80083b --- /dev/null +++ b/zephcore/boards/nrf52840/ikoka_nano_30dbm/board.conf @@ -0,0 +1,25 @@ +# Ikoka Nano 30dBm (XIAO nRF52840 + E22-900M30S) +# Board-specific configuration +# +# Hardware: +# - XIAO nRF52840 base board +# - E22-900M30S LoRa module (SX1262 + PA, 30dBm output) +# - Battery ADC on AIN7 (P0.31), enable on P0.14 +# - LEDs: RED=P0.26, BLUE=P0.06, GREEN=P0.30 +# +# LoRa Module E22-900M30S Notes: +# - Built-in 30dBm PA with RXEN control +# - Input power limited to 20dBm to prevent PA distortion +# - RXEN (P1.12) HIGH for RX, LOW for TX + +# Board identification (matches Arduino variant name) +CONFIG_ZEPHCORE_BOARD_NAME="Ikoka Nano-E22-30dBm (Xiao_nrf52)" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="Ikoka Nano 30dBm" + +# SoftDevice firmware ID (XIAO nRF52840 uses S140 v7.3.0) +CONFIG_ZEPHCORE_SD_FWID=0x0123 + +# Radio type +CONFIG_ZEPHCORE_RADIO_NATIVE=y diff --git a/zephcore/boards/nrf52840/ikoka_nano_30dbm/board.yml b/zephcore/boards/nrf52840/ikoka_nano_30dbm/board.yml new file mode 100644 index 0000000..985b987 --- /dev/null +++ b/zephcore/boards/nrf52840/ikoka_nano_30dbm/board.yml @@ -0,0 +1,6 @@ +board: + name: ikoka_nano_30dbm + full_name: Ikoka Nano 30dBm (XIAO nRF52840 + E22-900M30S) + vendor: ikoka + socs: + - name: nrf52840 diff --git a/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm-pinctrl.dtsi b/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm-pinctrl.dtsi new file mode 100644 index 0000000..1dc2312 --- /dev/null +++ b/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm-pinctrl.dtsi @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Ikoka Nano 30dBm pin control - based on XIAO nRF52840 + */ + +&pinctrl { + /* UART0 on D6/D7 (optional, shared with LoRa RXEN) */ + uart0_default: uart0_default { + group1 { + psels = ; /* D6 = P1.11 */ + }; + group2 { + psels = ; /* D7 = P1.12 */ + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* I2C1 on D4/D5 for external sensors */ + i2c1_default: i2c1_default { + group1 { + psels = , /* D4 = P0.04 */ + ; /* D5 = P0.05 */ + }; + }; + + i2c1_sleep: i2c1_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* SPI2 for LoRa on D8/D9/D10 */ + spi2_default: spi2_default { + group1 { + psels = , /* D8 = P1.13 */ + , /* D10 = P1.15 */ + ; /* D9 = P1.14 */ + }; + }; + + spi2_sleep: spi2_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; + + /* QSPI for onboard flash */ + qspi_default: qspi_default { + group1 { + psels = , + , + , + , + , + ; + }; + }; + + qspi_sleep: qspi_sleep { + group1 { + psels = , + , + , + , + ; + low-power-enable; + }; + group2 { + psels = ; + low-power-enable; + bias-pull-up; + }; + }; +}; diff --git a/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts b/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts new file mode 100644 index 0000000..2a26726 --- /dev/null +++ b/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts @@ -0,0 +1,223 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Ikoka Nano 30dBm - XIAO nRF52840 + E22-900M30S LoRa module + * + * E22-900M30S LoRa Module: + * - SX1262 with integrated 30dBm PA + * - RXEN (P1.12/D7) control: HIGH for RX, LOW for TX + * - Input power limited to 20dBm (30dBm output) + * - DIO2 as RF switch, TCXO 1.8V + * + * Pin Mapping (XIAO D-pins to nRF52840 GPIOs): + * - D0 = P0.02 (NSS) - D4 = P0.04 (SDA) - D8 = P1.13 (SCK) + * - D1 = P0.03 (DIO1) - D5 = P0.05 (SCL) - D9 = P1.14 (MISO) + * - D2 = P0.28 (BUSY) - D6 = P1.11 (TX) - D10 = P1.15 (MOSI) + * - D3 = P0.29 (RESET) - D7 = P1.12 (RXEN) + * + * Battery: P0.31/AIN7 with enable on P0.14 + * LEDs: RED=P0.26/D11, BLUE=P0.06/D12, GREEN=P0.30/D13 + */ + +/dts-v1/; +#include +#include "ikoka_nano_30dbm-pinctrl.dtsi" +#include + +/ { + model = "Ikoka Nano 30dBm"; + compatible = "ikoka,nano-30dbm"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &code_partition; + zephyr,console = &cdc_acm_uart; + zephyr,shell-uart = &cdc_acm_uart; + zephyr,settings-partition = &storage_partition; + }; + + aliases { + lora0 = &lora; + led0 = &led_red; + led1 = &led_green; + led2 = &led_blue; + watchdog0 = &wdt0; + }; + + /* Status LEDs - active LOW on XIAO */ + leds { + compatible = "gpio-leds"; + led_red: led_0 { + gpios = <&gpio0 26 GPIO_ACTIVE_LOW>; + label = "Red LED"; + }; + led_green: led_1 { + gpios = <&gpio0 30 GPIO_ACTIVE_LOW>; + label = "Green LED"; + }; + led_blue: led_2 { + gpios = <&gpio0 6 GPIO_ACTIVE_LOW>; + label = "Blue LED"; + }; + }; + + /* Battery ADC enable - P0.14 must be LOW to read battery */ + vbat_enable: vbat-enable { + compatible = "regulator-fixed"; + regulator-name = "vbat-enable"; + enable-gpios = <&gpio0 14 GPIO_ACTIVE_LOW>; + regulator-boot-on; + }; + + /* Battery ADC channel */ + zephyr,user { + io-channels = <&adc 7>; + vbat-mv-multiplier = <10650>; /* 1M/510k divider (2.96x), 3.6V ref */ + }; +}; + +®0 { + status = "okay"; +}; + +®1 { + regulator-initial-mode = ; +}; + +&adc { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + channel@7 { + reg = <7>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + }; +}; + +&uicr { + gpio-as-nreset; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +/* I2C1 on D4/D5 for external sensors */ +&i2c1 { + compatible = "nordic,nrf-twi"; + status = "okay"; + pinctrl-0 = <&i2c1_default>; + pinctrl-1 = <&i2c1_sleep>; + pinctrl-names = "default", "sleep"; + + /* All supported environment & power sensors — auto-detected at runtime */ + #include "../../common/sensors-i2c.dtsi" +}; + +/* SPI2 for LoRa module */ +&spi2 { + compatible = "nordic,nrf-spi"; + status = "okay"; + pinctrl-0 = <&spi2_default>; + pinctrl-1 = <&spi2_sleep>; + pinctrl-names = "default", "sleep"; + cs-gpios = <&gpio0 2 GPIO_ACTIVE_LOW>; /* D0 = P0.02 NSS */ + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <8000000>; + + /* LoRa control pins */ + reset-gpios = <&gpio0 29 GPIO_ACTIVE_LOW>; /* D3 = P0.29 */ + busy-gpios = <&gpio0 28 GPIO_ACTIVE_HIGH>; /* D2 = P0.28 */ + dio1-gpios = <&gpio0 3 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* D1 = P0.03 */ + + /* E22-900M30S PA control */ + rx-enable-gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>; /* D7 = P1.12 RXEN */ + + /* Use DIO2 to drive TX enable on RF switch */ + dio2-tx-enable; + + /* TCXO voltage 1.8V */ + dio3-tcxo-voltage = <6>; + tcxo-power-startup-delay-ms = <10>; + + /* Enable RX boosted mode for better sensitivity */ + rx-boosted; + }; +}; + +/* QSPI Flash - P25Q16H (2MB) on XIAO nRF52840 + * Disabled: Flash chip appears damaged or locked on this unit. + * Re-enable with status = "okay" if working QSPI flash is needed. */ +&qspi { + status = "disabled"; +}; + +/* USB CDC for console/CLI */ +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; + + cdc_acm_uart: cdc_acm_uart { + compatible = "zephyr,cdc-acm-uart"; + }; +}; + +/* LittleFS auto-mount — uses common config for /lfs */ +#include "../../common/filesystem.dtsi" +/* QSPI disabled on this board — see note above */ + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* SoftDevice 156KB (reserved) */ + boot_partition: partition@0 { + label = "softdevice"; + read-only; + reg = <0x00000000 0x00027000>; + }; + + /* Application 692KB */ + code_partition: partition@27000 { + label = "Application"; + reg = <0x00027000 0x000AD000>; + }; + + /* NVS 8KB */ + storage_partition: partition@d4000 { + label = "storage"; + reg = <0x000D4000 0x00002000>; + }; + + /* LittleFS 92KB */ + lfs_partition: partition@d6000 { + label = "lfs"; + reg = <0x000D6000 0x00017000>; + }; + + /* UF2 bootloader 76KB (reserved) */ + uf2_partition: partition@ed000 { + label = "UF2"; + read-only; + reg = <0x000ED000 0x00013000>; + }; + }; +}; diff --git a/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm_defconfig b/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm_defconfig new file mode 100644 index 0000000..f9da0b6 --- /dev/null +++ b/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm_defconfig @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: Apache-2.0 +# Ikoka Nano 30dBm default configuration + +# Enable MPU +CONFIG_ARM_MPU=y +CONFIG_HW_STACK_PROTECTION=y + +# Enable GPIO +CONFIG_GPIO=y + +# Enable console on USB CDC (new stack) +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y +CONFIG_SERIAL=y +CONFIG_UART_LINE_CTRL=y + +# Enable flash/NVS storage +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_NVS=y + +# Pinctrl +CONFIG_PINCTRL=y + +# SPI for LoRa +CONFIG_SPI=y + +# ADC for battery +CONFIG_ADC=y + diff --git a/zephcore/boards/nrf52840/rak4631/board.conf b/zephcore/boards/nrf52840/rak4631/board.conf new file mode 100644 index 0000000..41cb455 --- /dev/null +++ b/zephcore/boards/nrf52840/rak4631/board.conf @@ -0,0 +1,27 @@ +# RAK4631 (nRF52840 + SX1262) +# Board-specific configuration - pins and unique features only +# +# SoftDevice: s140 v6.1.1 (app@0x26000, bootloader@0xF4000) +# Arduino linker: nrf52840_s140_v6_extrafs.ld +# +# Hardware: +# - SX1262 LoRa on SPI0 (built into RAK4631 module) +# - RAK1910 GPS on UART0 (Slot A) - u-blox MAX-7Q at 9600 baud +# - Battery ADC on AIN3 (P0.05) +# - I2C0 for WisBlock sensors (auto-detected): +# - RAK1901 (SHTC3) at 0x70 +# - RAK1902 (LPS22HB) at 0x5C +# - RAK1906 (BME680) at 0x76 + +# Board identification (matches Arduino variant name) +CONFIG_ZEPHCORE_BOARD_NAME="RAK 4631" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="RAK4631" + +# Radio type +CONFIG_ZEPHCORE_RADIO_NATIVE=y + +# QSPI flash — disabled (RAK19003 Mini Base has no QSPI flash) +# For base boards with QSPI (RAK19007, RAK5005-O), create a separate variant. +CONFIG_NORDIC_QSPI_NOR=n diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay new file mode 100644 index 0000000..25a1f5f --- /dev/null +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -0,0 +1,155 @@ +/* + * RAK4631 overlay for SoftDevice + UF2 bootloader. + * App at 0x26000, storage at 0xD4000, bootloader at 0xF4000. + * Uses SoftDevice s140 v6.1.1 (ends at 0x26000). + * Console: USB CDC ACM. + * Battery: AIN3 = P0.05 (Arduino pin 5), voltage divider per RAK4631 schematic. + * + * QSPI Flash: Optional external flash on WisBlock Base boards. + * Supports: P25Q16H, MX25R1635F, W25Q16, GD25Q16, IS25LP080, ZD25WQ16 + * If no QSPI flash present, falls back to internal flash only. + * + * GPS: RAK1910 (u-blox MAX-7Q) on UART0 at 9600 baud (Slot A) + * Sensors: Auto-detected on I2C0 - supports: + * - RAK1901 (SHTC3) at 0x70 + * - RAK1902 (LPS22HB) at 0x5C + * - RAK1906 (BME680) at 0x76 + */ + +#include + +/delete-node/ &boot_partition; +/delete-node/ &slot0_partition; +/delete-node/ &slot1_partition; +/delete-node/ &storage_partition; + +/ { + aliases { + led1 = &green_led; + }; + chosen { + zephyr,code-partition = &code_partition; + zephyr,console = &cdc_acm_uart; + zephyr,shell-uart = &cdc_acm_uart; + zephyr,settings-partition = &storage_partition; + }; +}; + +/* Battery ADC - AIN3 on P0.05 with voltage divider */ +/ { + zephyr,user { + io-channels = <&adc 3>; + vbat-mv-multiplier = <4216>; /* RAK4631 voltage divider */ + }; +}; + +&adc { + #address-cells = <1>; + #size-cells = <0>; + channel@3 { + reg = <3>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + }; +}; + +&zephyr_udc0 { + cdc_acm_uart: cdc_acm_uart { + compatible = "zephyr,cdc-acm-uart"; + }; +}; + +&lora { + dio1-gpios = <&gpio1 15 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; +}; + +/* + * GPS on UART0 - RAK1910 (u-blox MAX-7Q) at 9600 baud + * Connect to Slot A (UART pins P0.19 RX, P0.20 TX) + */ +&uart0 { + current-speed = <9600>; + gnss: gnss-nmea-generic { + compatible = "gnss-nmea-generic"; + }; +}; + +/* + * Environment & power sensors on I2C0 + * All supported sensors pre-defined — auto-detected at runtime. + * WisBlock modules: RAK1901 (SHTC3), RAK1902 (LPS22HB), RAK1906 (BME680) + */ +&i2c0 { + #include "../../common/sensors-i2c.dtsi" +}; + +/* RAK4631 has BME680, not BME280 — override the common DTSI default */ +/delete-node/ &bme280; +&i2c0 { + bme680: bme680@76 { + compatible = "bosch,bme680"; + reg = <0x76>; + }; +}; + +/* + * QSPI Flash — disabled. + * RAK19003 Mini Base Board has no QSPI flash. The upstream DTS enables &qspi, + * which hangs at boot if no chip is present. Disable it explicitly. + * For base boards with QSPI (RAK19007, RAK5005-O), create a separate variant. + */ +&qspi { + status = "disabled"; +}; + +/* Filesystem mounts - internal /lfs only (no QSPI) */ +#include "../../common/filesystem.dtsi" +&flash0 { + partitions { + /* + * SoftDevice + UF2 bootloader partition layout + * Total: 1MB internal flash + * + * 0x00000 - 0x26000 (152KB) SoftDevice s140 v6.1.1 (reserved) + * 0x26000 - 0xD4000 (696KB) Application + * 0xD4000 - 0xD6000 (8KB) NVS storage (BLE bonds) + * 0xD6000 - 0xF4000 (120KB) LittleFS (identity, prefs) + * 0xF4000 - 0x100000 (48KB) UF2 bootloader (reserved) + */ + + /* SoftDevice 152KB (reserved, managed by bootloader) */ + boot_partition: partition@0 { + label = "softdevice"; + read-only; + reg = <0x00000000 0x00026000>; + }; + + /* Application 696KB */ + code_partition: partition@26000 { + label = "Application"; + reg = <0x00026000 0x000AE000>; + }; + + /* NVS 8KB for BLE bonds */ + storage_partition: partition@d4000 { + label = "storage"; + reg = <0x000D4000 0x00002000>; + }; + + /* LittleFS 120KB for DataStore (identity, contacts, channels, etc) */ + lfs_partition: partition@d6000 { + label = "lfs"; + reg = <0x000D6000 0x0001E000>; + }; + + /* UF2 bootloader 48KB (reserved) */ + uf2_partition: partition@f4000 { + label = "UF2"; + read-only; + reg = <0x000F4000 0x0000C000>; + }; + }; +}; diff --git a/zephcore/boards/nrf52840/t1000_e/Kconfig.t1000_e b/zephcore/boards/nrf52840/t1000_e/Kconfig.t1000_e new file mode 100644 index 0000000..de81e3b --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/Kconfig.t1000_e @@ -0,0 +1,6 @@ +# Copyright (c) 2025 MeshCore +# +# SPDX-License-Identifier: Apache-2.0 + +config BOARD_T1000_E + select SOC_NRF52840_QIAA diff --git a/zephcore/boards/nrf52840/t1000_e/board.cmake b/zephcore/boards/nrf52840/t1000_e/board.cmake new file mode 100644 index 0000000..d05119f --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/board.cmake @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +# T1000-E uses J-Link or UF2 bootloader +board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000") +board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000") + +include(${ZEPHYR_BASE}/boards/common/uf2.board.cmake) +include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) diff --git a/zephcore/boards/nrf52840/t1000_e/board.conf b/zephcore/boards/nrf52840/t1000_e/board.conf new file mode 100644 index 0000000..f4e9361 --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/board.conf @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: Apache-2.0 +# Seeed Tracker T1000-E board configuration +# +# Hardware: +# - LR1110 LoRa on SPI (P0.12 CS, P1.10 RST, P0.07 BUSY, P1.01 DIO1) +# - L76K GPS on UART0 (P0.13 TX, P0.14 RX), enable P1.11 +# - Battery ADC on AIN0 (P0.02), 2.0x voltage divider +# - LED green (P0.24), Button (P0.06) + +# Board identification (matches Arduino variant name) +CONFIG_ZEPHCORE_BOARD_NAME="T1000-E" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="Seeed T1000-E" + +# Radio type (TCXO, RF switch, PA config in DTS) +CONFIG_ZEPHCORE_RADIO_LR1110=y + +# SPI for LR1110 +CONFIG_SPI=y + +# PWM for buzzer +CONFIG_PWM=y diff --git a/zephcore/boards/nrf52840/t1000_e/board.overlay b/zephcore/boards/nrf52840/t1000_e/board.overlay new file mode 100644 index 0000000..1c70a03 --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/board.overlay @@ -0,0 +1,55 @@ +/* + * T1000-E board overlay - additional ZephCore-specific configuration + * SPDX-License-Identifier: Apache-2.0 + * + * GPS control pins for Airoha AG3335 GNSS module (matches Arduino target.cpp): + * - GPS_EN (P1.11) - Main power enable + * - GPS_VRTC_EN (P0.8) - RTC voltage regulator (critical!) + * - GPS_RESET (P1.15) - Reset control + * - GPS_SLEEP_INT (P1.12) - Sleep interrupt + */ + +/ { + /* GPS power enable pin - P1.11 */ + gps_en: gps-enable { + compatible = "gpio-leds"; + gps_enable_pin: gps_enable { + gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>; + label = "GPS Enable"; + }; + }; + + /* GPS RTC voltage regulator enable - P0.8 (critical for GPS operation) */ + gps_vrtc: gps-vrtc-enable { + compatible = "gpio-leds"; + gps_vrtc_pin: gps_vrtc { + gpios = <&gpio0 8 GPIO_ACTIVE_HIGH>; + label = "GPS VRTC Enable"; + }; + }; + + /* GPS reset control - P1.15 */ + gps_rst: gps-reset { + compatible = "gpio-leds"; + gps_reset_pin: gps_reset { + gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>; + label = "GPS Reset"; + }; + }; + + /* GPS sleep interrupt - P1.12 */ + gps_sleep: gps-sleep-int { + compatible = "gpio-leds"; + gps_sleep_pin: gps_sleep { + gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>; + label = "GPS Sleep Int"; + }; + }; + + aliases { + gps-enable = &gps_enable_pin; + gps-vrtc-enable = &gps_vrtc_pin; + gps-reset = &gps_reset_pin; + gps-sleep-int = &gps_sleep_pin; + }; +}; diff --git a/zephcore/boards/nrf52840/t1000_e/board.yml b/zephcore/boards/nrf52840/t1000_e/board.yml new file mode 100644 index 0000000..9a5380f --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/board.yml @@ -0,0 +1,10 @@ +# Copyright (c) 2025 MeshCore +# +# SPDX-License-Identifier: Apache-2.0 + +board: + name: t1000_e + full_name: Seeed Tracker T1000-E + vendor: seeed + socs: + - name: nrf52840 diff --git a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840-pinctrl.dtsi b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840-pinctrl.dtsi new file mode 100644 index 0000000..1f0f789 --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840-pinctrl.dtsi @@ -0,0 +1,80 @@ +/* + * Seeed Tracker T1000-E - Pin Control + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Pin assignments from variant.h: + * - SPI: SCK=P0.11, MISO=P1.08, MOSI=P1.09, NSS=P0.12 (for LR1110) + * - I2C: SDA=P0.26, SCL=P0.27 + * - UART: TX=P0.13, RX=P0.14 (GPS L76K) + * - LR1110: DIO1=P1.01, BUSY=P0.07, RESET=P1.10 + */ + +&pinctrl { + /* GPS UART - L76K at 9600 baud */ + uart0_default: uart0_default { + group1 { + psels = ; + }; + group2 { + psels = ; + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* I2C0 for accelerometer */ + i2c0_default: i2c0_default { + group1 { + psels = , + ; + }; + }; + + i2c0_sleep: i2c0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* PWM0 for buzzer on P0.25 */ + pwm0_default: pwm0_default { + group1 { + psels = ; + }; + }; + + pwm0_sleep: pwm0_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; + + /* SPI1 for LR1110 - CS controlled manually */ + spi1_default: spi1_default { + group1 { + psels = , + , + ; + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; +}; diff --git a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts new file mode 100644 index 0000000..b1ea22f --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts @@ -0,0 +1,325 @@ +/* + * Seeed Tracker T1000-E - nRF52840 + LR1110 + * Copyright (c) 2025 ZephCore + * + * SPDX-License-Identifier: Apache-2.0 + * + * Hardware: + * - nRF52840 SoC with BLE + * - LR1110 LoRa radio (TCXO 1.6V, DIO as RF switch) + * - Airoha AG3335 GNSS module on UART (PAIR protocol) + * - QMA6100P accelerometer (I2C) + * - NTC temperature sensor (ADC) + * - Light sensor (ADC) + * - Buzzer with enable + * - Battery voltage ADC + */ + +/dts-v1/; +#include +#include +#include +#include +#include +#include "t1000_e_nrf52840-pinctrl.dtsi" + +/* Delete default partitions from nrf52840_partition.dtsi */ +/delete-node/ &boot_partition; +/delete-node/ &slot0_partition; +/delete-node/ &slot1_partition; +/delete-node/ &storage_partition; + +/ { + model = "Seeed Tracker T1000-E"; + compatible = "seeed,t1000-e"; + + chosen { + zephyr,code-partition = &slot0_partition; + zephyr,console = &cdc_acm_uart; + zephyr,shell-uart = &cdc_acm_uart; + zephyr,settings-partition = &storage_partition; + }; + + leds { + compatible = "gpio-leds"; + + led_green: led_0 { + gpios = <&gpio0 24 GPIO_ACTIVE_HIGH>; + label = "Green LED"; + }; + }; + + buttons: buttons { + compatible = "gpio-keys"; + + user_button: button_0 { + /* T1000-E button: active HIGH (pressed=VCC), no internal pull + * (external pull-down on board). Arduino uses INPUT, not INPUT_PULLUP. */ + gpios = <&gpio0 6 GPIO_ACTIVE_HIGH>; + zephyr,code = ; + label = "User Button"; + }; + }; + + /* Input filter: user button long-press detection (1000ms) + * T1000-E has only one button - long-press = enter/execute + * Short-press → KEY_A, Long-press → KEY_F (safe, no system side-effects) */ + user_btn_longpress { + compatible = "zephyr,input-longpress"; + input = <&buttons>; + input-codes = ; + short-codes = ; + long-codes = ; + long-delay-ms = <1000>; + }; + + /* Input filter: user button multi-tap detection + * 1 tap (400ms wait) → KEY_1 = page next (no-op without display) + * 2 taps (400ms wait) → KEY_B = flood advert + * 3 taps (400ms wait) → KEY_D = buzzer mute toggle + * 4 taps (immediate) → KEY_C = GPS on/off */ + user_btn_multitap { + compatible = "zephcore,input-multi-tap"; + /* No 'input' phandle — listen to ALL devices. + * KEY_A is emitted by the longpress pseudo-device, + * not by &buttons, so we must not filter by device. */ + input-codes = ; + tap-codes = ; + tap-delay-ms = <400>; + }; + + /* Sensor power enable - P1.06 (PIN_3V3_EN in Arduino) + * Powers: Battery ADC voltage divider, temperature sensor, light sensor + * Must be enabled before reading battery voltage or sensor ADCs + */ + vbat_enable: sensor_power: sensor-power { + compatible = "regulator-fixed"; + regulator-name = "sensor-power"; + enable-gpios = <&gpio1 6 GPIO_ACTIVE_HIGH>; + /* NOT regulator-boot-on - enabled on-demand in ZephyrBoard.cpp */ + }; + + /* Battery ADC channel reference for ZephyrBoard */ + zephyr,user { + io-channels = <&adc 0>; + vbat-mv-multiplier = <7200>; /* 2:1 voltage divider, 3.6V ref */ + }; + + /* Buzzer on PWM0 channel 0 (P0.25) with enable on P1.05 */ + pwmbuzzer { + compatible = "pwm-leds"; + buzzer: buzzer { + pwms = <&pwm0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>; + }; + }; + + /* Buzzer enable pin - controls power to buzzer amplifier */ + buzzer_enable: buzzer-enable { + compatible = "regulator-fixed"; + regulator-name = "buzzer-enable"; + enable-gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>; + /* NOT regulator-boot-on - controlled by buzzer driver */ + }; + + aliases { + led0 = &led_green; + sw0 = &user_button; + watchdog0 = &wdt0; + buzzer = &buzzer; + buzzer-enable = &buzzer_enable; + lora0 = &lora; + }; +}; + +®0 { + status = "okay"; +}; + +®1 { + regulator-initial-mode = ; +}; + +&adc { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + /* Battery voltage on P0.02 (AIN0) - 2x voltage divider */ + channel@0 { + reg = <0>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + }; + + /* Temperature sensor NTC on P0.31 (AIN7) */ + channel@7 { + reg = <7>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + }; + + /* Light sensor on P0.29 (AIN5) */ + channel@5 { + reg = <5>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + }; +}; + +&uicr { + nfct-pins-as-gpios; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; + /* NOTE: buzzer-off gpio-hog REMOVED - buzzer enable is now managed by + * the regulator-fixed node (buzzer_enable). When buzzer is quiet, + * the regulator is disabled keeping the pin low. */ +}; + +/* PWM0 for buzzer */ +&pwm0 { + status = "okay"; + pinctrl-0 = <&pwm0_default>; + pinctrl-1 = <&pwm0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +/* Airoha AG3335 GNSS on UART0 - 115200 baud (PAIR protocol) + * Uses Zephyr quectel,lc76g driver (LC76G is built on AG3335, + * same PAIR command set for constellation config, fix rate, etc.) */ +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <115200>; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; + + gnss: gnss { + compatible = "quectel,lc76g"; + pps-mode = "GNSS_PPS_MODE_DISABLED"; + }; +}; + +/* I2C0 for QMA6100P accelerometer + external sensors */ +&i2c0 { + compatible = "nordic,nrf-twim"; + status = "okay"; + clock-frequency = ; + pinctrl-0 = <&i2c0_default>; + pinctrl-1 = <&i2c0_sleep>; + pinctrl-names = "default", "sleep"; + + /* All supported environment & power sensors — auto-detected at runtime */ + #include "../../common/sensors-i2c.dtsi" +}; + +/* SPI1 for LR1110 */ +&spi1 { + compatible = "nordic,nrf-spim"; + status = "okay"; + cs-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&spi1_default>; + pinctrl-1 = <&spi1_sleep>; + pinctrl-names = "default", "sleep"; + + lora: lora@0 { + compatible = "semtech,lr1110"; + reg = <0>; + reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio0 7 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio1 1 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + tcxo-voltage-mv = <1600>; + tcxo-startup-delay-ms = <5>; + rx-boosted; + spi-max-frequency = <8000000>; + /* RF switch: DIO5=RFSW0, DIO6=RFSW1, DIO7=RFSW2, DIO8=RFSW3 */ + rfswitch-enable = <0x0F>; + rfswitch-standby = <0x00>; + rfswitch-rx = <0x09>; + rfswitch-tx = <0x0B>; + rfswitch-tx-hp = <0x0A>; + rfswitch-gnss = <0x04>; + }; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; + + cdc_acm_uart: cdc_acm_uart { + compatible = "zephyr,cdc-acm-uart"; + }; +}; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* + * SoftDevice + UF2 bootloader partition layout + * Total: 1MB internal flash + * + * 0x00000 - 0x27000 (156KB) SoftDevice s140 v7 (reserved) + * 0x27000 - 0xD4000 (692KB) Application + * 0xD4000 - 0xD6000 (8KB) NVS storage (BLE bonds) + * 0xD6000 - 0xED000 (92KB) LittleFS (identity, prefs) + * 0xED000 - 0x100000 (76KB) UF2 bootloader (reserved) + */ + + /* SoftDevice 156KB (reserved, managed by bootloader) */ + boot_partition: partition@0 { + label = "softdevice"; + read-only; + reg = <0x00000000 0x00027000>; + }; + + /* Application 692KB */ + slot0_partition: partition@27000 { + label = "Application"; + reg = <0x00027000 0x000AD000>; + }; + + /* NVS 8KB for BLE bonds */ + storage_partition: partition@d4000 { + label = "storage"; + reg = <0x000D4000 0x00002000>; + }; + + /* LittleFS 92KB for DataStore (identity, contacts, channels, etc) */ + lfs_partition: partition@d6000 { + label = "lfs"; + reg = <0x000D6000 0x00017000>; + }; + + /* UF2 bootloader 76KB (reserved) */ + uf2_partition: partition@ed000 { + label = "UF2"; + read-only; + reg = <0x000ED000 0x00013000>; + }; + }; +}; + +/* LittleFS auto-mount — uses common config for /lfs */ +#include "../../common/filesystem.dtsi" diff --git a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.yaml b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.yaml new file mode 100644 index 0000000..cb1f0d8 --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.yaml @@ -0,0 +1,20 @@ +identifier: t1000_e +name: Seeed Tracker T1000-E +type: mcu +arch: arm +ram: 256 +flash: 1024 +toolchain: + - zephyr + - gnuarmemb + - xtools +supported: + - adc + - ble + - gpio + - i2c + - nvs + - spi + - usb_device + - watchdog +vendor: seeed diff --git a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840_defconfig b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840_defconfig new file mode 100644 index 0000000..cae0a87 --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840_defconfig @@ -0,0 +1,21 @@ +# Copyright (c) 2025 MeshCore +# SPDX-License-Identifier: Apache-2.0 + +# Enable MPU +CONFIG_ARM_MPU=y + +# Enable hardware stack protection +CONFIG_HW_STACK_PROTECTION=y + +# Enable GPIO +CONFIG_GPIO=y + +# Enable UART driver +CONFIG_SERIAL=y + +# Enable console +CONFIG_CONSOLE=y + +# Build UF2 by default +CONFIG_BUILD_OUTPUT_UF2=y +CONFIG_USE_DT_CODE_PARTITION=y diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/Kconfig.wio_tracker_l1 b/zephcore/boards/nrf52840/wio_tracker_l1/Kconfig.wio_tracker_l1 new file mode 100644 index 0000000..6783394 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/Kconfig.wio_tracker_l1 @@ -0,0 +1,6 @@ +# Copyright (c) 2025 MeshCore +# +# SPDX-License-Identifier: Apache-2.0 + +config BOARD_WIO_TRACKER_L1 + select SOC_NRF52840_QIAA diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/board.cmake b/zephcore/boards/nrf52840/wio_tracker_l1/board.cmake new file mode 100644 index 0000000..66c6ba8 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/board.cmake @@ -0,0 +1,14 @@ +# Copyright (c) 2025 ZephCore +# +# SPDX-License-Identifier: Apache-2.0 + +board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000") +board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000") + +set(OPENOCD_NRF5_SUBFAMILY "nrf52") + +include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) +include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake) +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) +include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake) diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/board.conf b/zephcore/boards/nrf52840/wio_tracker_l1/board.conf new file mode 100644 index 0000000..c4ef4fd --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/board.conf @@ -0,0 +1,26 @@ +# Wio Tracker L1 (nRF52840 + SX1262) +# Board-specific configuration - pins and unique features only +# +# Hardware: +# - SX1262 LoRa on SPI0 (P1.14 CS, P1.07 RST, P1.10 BUSY, P0.07 DIO1, P1.08 RXEN) +# - L76KB GPS on UART0 (P0.26/P0.27) +# - P25Q16H 2MB QSPI flash +# - Battery ADC on AIN7 (P0.31), enable on P0.04 +# - SH1106 OLED on I2C0 (P0.05/P0.06, addr 0x3D) +# - Joystick + user button + buzzer (P1.00) + +# Board identification (matches Arduino variant name) +CONFIG_ZEPHCORE_BOARD_NAME="Wio Tracker L1" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="Wio Tracker 1110" + +# Radio type +CONFIG_ZEPHCORE_RADIO_NATIVE=y + +# QSPI flash present +CONFIG_NORDIC_QSPI_NOR=y +CONFIG_NORDIC_QSPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096 + +# PWM for buzzer +CONFIG_PWM=y diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay b/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay new file mode 100644 index 0000000..4a67d79 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay @@ -0,0 +1,28 @@ +/* + * Wio Tracker L1 overlay for ZephCore. + * SoftDevice + UF2 bootloader layout. + * Console: USB CDC ACM. + * Battery: AIN7 = P0.31, voltage divider 2:1. + */ + +/* GPS power control - P1.09 + * The luatos,air530z driver also manages this pin via on-off-gpios, + * but we need our own alias so ZephyrSensorManager can toggle power + * independently (driver PM suspend/resume hangs on modem_pipe ops). */ +/ { + gps_en: gps-enable { + compatible = "gpio-leds"; + gps_enable_pin: gps_enable { + gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>; + label = "GPS Enable"; + }; + }; + + aliases { + gps-enable = &gps_enable_pin; + }; +}; + +/* Filesystem mounts - internal /lfs + external /ext QSPI */ +#include "../../common/filesystem.dtsi" +#include "../../common/qspi-ext.dtsi" diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/board.yml b/zephcore/boards/nrf52840/wio_tracker_l1/board.yml new file mode 100644 index 0000000..3c42e7f --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/board.yml @@ -0,0 +1,10 @@ +# Copyright (c) 2025 MeshCore +# +# SPDX-License-Identifier: Apache-2.0 + +board: + name: wio_tracker_l1 + full_name: Seeed Wio Tracker L1 + vendor: seeed + socs: + - name: nrf52840 diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840-pinctrl.dtsi b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840-pinctrl.dtsi new file mode 100644 index 0000000..c24b792 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840-pinctrl.dtsi @@ -0,0 +1,123 @@ +/* + * Seeed Wio Tracker L1 pin control definitions + * Copyright (c) 2025 ZephCore + * + * SPDX-License-Identifier: Apache-2.0 + * + * Pin mappings from Arduino variant.cpp: + * - SPI0 (LoRa): SCK=P0.30, MISO=P0.03, MOSI=P0.28, CS=P1.14 + * - I2C0 (OLED): SDA=P0.06, SCL=P0.05 + * - I2C1 (Grove): SDA=P1.11 (D17), SCL=P1.12 (D18) + * - UART (GPS): TX=P0.27, RX=P0.26 + * - QSPI: SCK=P0.21, CS=P0.25, IO0=P0.20, IO1=P0.24, IO2=P0.22, IO3=P0.23 + */ + +&pinctrl { + /* SPI1 for LoRa SX1262 (uses SPI1 to avoid conflict with I2C0) */ + spi1_default: spi1_default { + group1 { + psels = , + , + ; + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; + + /* I2C0 for OLED display */ + i2c0_default: i2c0_default { + group1 { + psels = , + ; + }; + }; + + i2c0_sleep: i2c0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* I2C1 for Grove connector */ + i2c1_default: i2c1_default { + group1 { + psels = , + ; + }; + }; + + i2c1_sleep: i2c1_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* UART0 for GPS L76KB at 9600 baud + * P0.27 = GNSS_TX (GPS sends to MCU) + * P0.26 = GNSS_RX (MCU sends to GPS) + */ + uart0_default: uart0_default { + group1 { + psels = , + ; + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* PWM0 for buzzer on P1.00 */ + pwm0_default: pwm0_default { + group1 { + psels = ; + }; + }; + + pwm0_sleep: pwm0_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; + + /* QSPI for external flash */ + qspi_default: qspi_default { + group1 { + psels = , + , + , + , + , + ; + }; + }; + + qspi_sleep: qspi_sleep { + group1 { + psels = , + , + , + , + , + ; + low-power-enable; + }; + }; +}; diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts new file mode 100644 index 0000000..8702307 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts @@ -0,0 +1,390 @@ +/* + * Seeed Wio Tracker L1 - nRF52840 + SX1262 + * Copyright (c) 2025 ZephCore + * + * SPDX-License-Identifier: Apache-2.0 + * + * Hardware: + * - nRF52840 SoC with BLE + * - SX1262 LoRa radio (DIO2 as RF switch, TCXO 1.8V) + * - P25Q16H 2MB QSPI flash + * - SH1106 OLED display (I2C 0x3D) + * - L76KB GPS module + * - Joystick + user button + * - Battery voltage ADC + */ + +/dts-v1/; +#include +#include +#include +#include +#include +#include +#include "wio_tracker_l1_nrf52840-pinctrl.dtsi" + +/* Delete default partitions from nrf52840_partition.dtsi */ +/delete-node/ &boot_partition; +/delete-node/ &slot0_partition; +/delete-node/ &slot1_partition; +/delete-node/ &storage_partition; + +/ { + model = "Seeed Wio Tracker L1"; + compatible = "seeed,wio-tracker-l1"; + + chosen { + zephyr,code-partition = &slot0_partition; + zephyr,console = &cdc_acm_uart; + zephyr,shell-uart = &cdc_acm_uart; + zephyr,settings-partition = &storage_partition; + zephyr,display = &sh1106; + }; + + leds { + compatible = "gpio-leds"; + + user_led: led_0 { + gpios = <&gpio1 1 GPIO_ACTIVE_HIGH>; + label = "User LED"; + }; + }; + + buttons: buttons { + compatible = "gpio-keys"; + + user_button: button_0 { + gpios = <&gpio0 8 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "User Button"; + }; + + joystick_up: button_1 { + gpios = <&gpio1 4 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "Joystick Up"; + }; + + joystick_down: button_2 { + gpios = <&gpio0 12 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "Joystick Down"; + }; + + joystick_left: button_3 { + gpios = <&gpio0 11 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "Joystick Left"; + }; + + joystick_right: button_4 { + gpios = <&gpio1 3 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "Joystick Right"; + }; + + joystick_press: button_5 { + gpios = <&gpio1 5 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "Joystick Press"; + }; + }; + + /* Input filter: user button long-press detection (1000ms) */ + user_btn_longpress { + compatible = "zephyr,input-longpress"; + input = <&buttons>; + input-codes = ; + short-codes = ; + long-codes = ; + long-delay-ms = <1000>; + }; + + /* Input filter: user button multi-tap detection + * 1 tap (400ms wait) → KEY_1 = page next on OLED + * 2 taps (400ms wait) → KEY_B = flood advert + * 3 taps (400ms wait) → KEY_D = buzzer mute toggle + * 4 taps (immediate) → KEY_C = GPS on/off */ + user_btn_multitap { + compatible = "zephcore,input-multi-tap"; + /* No 'input' phandle — listen to ALL devices. + * KEY_A is emitted by the longpress pseudo-device, + * not by &buttons, so we must not filter by device. */ + input-codes = ; + tap-codes = ; + tap-delay-ms = <400>; + }; + + /* Battery voltage enable and ADC - on-demand only to save power */ + vbat_enable: vbat-enable { + compatible = "regulator-fixed"; + regulator-name = "vbat-enable"; + enable-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>; + /* NOT regulator-boot-on - enabled on-demand in ZephyrBoard.cpp */ + }; + + /* Battery ADC channel reference for ZephyrBoard */ + zephyr,user { + io-channels = <&adc 7>; + vbat-mv-multiplier = <7200>; /* 2:1 voltage divider, 3.6V ref */ + }; + + /* Buzzer on PWM0 channel 0 (P1.00) - no enable pin needed */ + pwmbuzzer { + compatible = "pwm-leds"; + buzzer: buzzer { + pwms = <&pwm0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>; + }; + }; + + aliases { + led0 = &user_led; + sw0 = &user_button; + lora0 = &lora; + watchdog0 = &wdt0; + buzzer = &buzzer; + }; +}; + +®0 { + status = "okay"; +}; + +®1 { + regulator-initial-mode = ; +}; + +&adc { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + /* Battery voltage on P0.31 (AIN7) */ + channel@7 { + reg = <7>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + }; +}; + +&uicr { + nfct-pins-as-gpios; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; + + /* NOTE: buzzer-off gpio-hog REMOVED - buzzer pin is now managed by PWM + * driver. When PWM is not driving, the pin is low (no current draw). */ + + /* NOTE: GPS enable hog REMOVED - Feb 6 working version had no GPIO hog + * and GPS worked fine. The L76KB module may have internal pull-up or + * the Arduino bootloader leaves it enabled. */ +}; + +/* PWM0 for buzzer */ +&pwm0 { + status = "okay"; + pinctrl-0 = <&pwm0_default>; + pinctrl-1 = <&pwm0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +/* GPS L76KB module - NOTE: Feb 6 working version had no GPIO control nodes. + * The GPS just worked without any power control! Keeping it simple. */ + +/* Quectel L76KB GNSS on UART0 - 9600 baud (PCAS/CASIC protocol) + * Uses Zephyr luatos,air530z driver (same PCAS command set for + * constellation config via PCAS04, fix rate via PCAS02, etc.) + * on-off-gpios = GPS enable pin (P1.09), managed by driver. */ +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <9600>; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; + /* No hw-flow-control - GPS doesn't use RTS/CTS */ + + gnss: gnss { + compatible = "luatos,air530z"; + on-off-gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>; + }; +}; + +/* I2C0 for OLED display + sensors */ +&i2c0 { + compatible = "nordic,nrf-twim"; + status = "okay"; + clock-frequency = ; + pinctrl-0 = <&i2c0_default>; + pinctrl-1 = <&i2c0_sleep>; + pinctrl-names = "default", "sleep"; + /* SH1106 sends 1 cmd + 128 data = 129 bytes per page write. + * nRF TWIM needs an internal buffer >= this size. */ + zephyr,concat-buf-size = <256>; + zephyr,flash-buf-max-size = <256>; + + sh1106: sh1106@3d { + compatible = "sinowealth,sh1106"; + reg = <0x3d>; + width = <128>; + height = <64>; + segment-offset = <2>; /* SH1106 has 2-column offset */ + page-offset = <0>; + display-offset = <0>; + multiplex-ratio = <63>; + segment-remap; + com-invdir; + prechargep = <0x22>; + }; + + /* All supported environment & power sensors — auto-detected at runtime */ + #include "../../common/sensors-i2c.dtsi" +}; + +/* I2C1 for Grove connector - disabled by default */ +&i2c1 { + compatible = "nordic,nrf-twim"; + status = "disabled"; + pinctrl-0 = <&i2c1_default>; + pinctrl-1 = <&i2c1_sleep>; + pinctrl-names = "default", "sleep"; +}; + +/* SPI1 for LoRa SX1262 (SPI0 conflicts with I2C0 on same serial instance) */ +&spi1 { + compatible = "nordic,nrf-spim"; + status = "okay"; + cs-gpios = <&gpio1 14 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&spi1_default>; + pinctrl-1 = <&spi1_sleep>; + pinctrl-names = "default", "sleep"; + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio1 10 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio0 7 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + /* RXEN on P1.08, DIO2 as RF switch for TX */ + rx-enable-gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>; + dio2-tx-enable; + dio3-tcxo-voltage = ; + tcxo-power-startup-delay-ms = <5>; + spi-max-frequency = <8000000>; + }; +}; + +/* QSPI Flash - P25Q16H 2MB - single-bit mode at 16MHz (matches Arduino) */ +&qspi { + status = "okay"; + pinctrl-0 = <&qspi_default>; + pinctrl-1 = <&qspi_sleep>; + pinctrl-names = "default", "sleep"; + + qspi_flash: qspi_flash@0 { + compatible = "nordic,qspi-nor"; + reg = <0>; + writeoc = "pp"; /* Single-bit page program (universal) */ + readoc = "fastread"; /* Single-bit fast read (universal) */ + sck-frequency = <16000000>; /* 16 MHz (matches Arduino) */ + jedec-id = [85 60 15]; /* P25Q16H */ + sfdp-bfp = [ + e5 20 f1 ff ff ff ff 01 44 eb 08 6b 08 3b 04 bb + ee ff ff ff ff ff 00 ff ff ff 00 ff 0c 20 0f 52 + 10 d8 00 ff + ]; + size = <16777216>; /* 2MB = 16Mbit */ + has-dpd; + t-enter-dpd = <3000>; + t-exit-dpd = <50000>; /* 50ms for reliable wakeup */ + /* quad-enable not needed for single-bit mode */ + }; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; + + cdc_acm_uart: cdc_acm_uart { + compatible = "zephyr,cdc-acm-uart"; + }; +}; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* + * SoftDevice + UF2 bootloader partition layout + * Total: 1MB internal flash + * + * 0x00000 - 0x27000 (156KB) SoftDevice s140 v7 (reserved) + * 0x27000 - 0xD4000 (692KB) Application + * 0xD4000 - 0xD6000 (8KB) NVS storage (BLE bonds) + * 0xD6000 - 0xED000 (92KB) LittleFS (identity, prefs) + * 0xED000 - 0x100000 (76KB) UF2 bootloader (reserved) + */ + + /* SoftDevice 156KB (reserved, managed by bootloader) */ + boot_partition: partition@0 { + label = "softdevice"; + read-only; + reg = <0x00000000 0x00027000>; + }; + + /* Application 692KB */ + slot0_partition: partition@27000 { + label = "Application"; + reg = <0x00027000 0x000AD000>; + }; + + /* NVS 8KB for BLE bonds */ + storage_partition: partition@d4000 { + label = "storage"; + reg = <0x000D4000 0x00002000>; + }; + + /* LittleFS 92KB for DataStore (identity, contacts, channels, etc) */ + lfs_partition: partition@d6000 { + label = "lfs"; + reg = <0x000D6000 0x00017000>; + }; + + /* UF2 bootloader 76KB (reserved) */ + uf2_partition: partition@ed000 { + label = "UF2"; + read-only; + reg = <0x000ED000 0x00013000>; + }; + }; +}; + +/* External QSPI flash partitions */ +&qspi_flash { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* Full 2MB for LittleFS storage */ + qspi_storage_partition: partition@0 { + label = "qspi_storage"; + reg = <0x00000000 0x00200000>; + }; + }; +}; diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.yaml b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.yaml new file mode 100644 index 0000000..4f86e39 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.yaml @@ -0,0 +1,20 @@ +identifier: wio_tracker_l1/nrf52840 +name: Seeed Wio Tracker L1 +type: mcu +arch: arm +toolchain: + - zephyr + - gnuarmemb + - xtools +ram: 256 +flash: 1024 +supported: + - adc + - ble + - gpio + - i2c + - spi + - usb_device + - qspi + - lora +vendor: seeed diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840_defconfig b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840_defconfig new file mode 100644 index 0000000..cae0a87 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840_defconfig @@ -0,0 +1,21 @@ +# Copyright (c) 2025 MeshCore +# SPDX-License-Identifier: Apache-2.0 + +# Enable MPU +CONFIG_ARM_MPU=y + +# Enable hardware stack protection +CONFIG_HW_STACK_PROTECTION=y + +# Enable GPIO +CONFIG_GPIO=y + +# Enable UART driver +CONFIG_SERIAL=y + +# Enable console +CONFIG_CONSOLE=y + +# Build UF2 by default +CONFIG_BUILD_OUTPUT_UF2=y +CONFIG_USE_DT_CODE_PARTITION=y diff --git a/zephcore/boards/nrf54l/xiao_nrf54l15/board.conf b/zephcore/boards/nrf54l/xiao_nrf54l15/board.conf new file mode 100644 index 0000000..9687e48 --- /dev/null +++ b/zephcore/boards/nrf54l/xiao_nrf54l15/board.conf @@ -0,0 +1,11 @@ +# Seeed XIAO nRF54L15 + Wio-SX1262 +# Board-specific configuration + +# Board identification +CONFIG_ZEPHCORE_BOARD_NAME="XIAO nRF54L15" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="XIAO nRF54L15" + +# Radio type +CONFIG_ZEPHCORE_RADIO_NATIVE=y diff --git a/zephcore/boards/nrf54l/xiao_nrf54l15/board.overlay b/zephcore/boards/nrf54l/xiao_nrf54l15/board.overlay new file mode 100644 index 0000000..26d7b96 --- /dev/null +++ b/zephcore/boards/nrf54l/xiao_nrf54l15/board.overlay @@ -0,0 +1,98 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * XIAO nRF54L15 + Wio-SX1262 LoRa + * + * Pin mapping (XIAO nRF54L15 connector → Wio-SX1262): + * - D1 (P1.05) = DIO1 - D8 (P2.01) = SPI SCK (spi00) + * - D2 (P1.06) = RESET - D9 (P2.04) = SPI MISO (spi00) + * - D3 (P1.07) = BUSY - D10 (P2.02) = SPI MOSI (spi00) + * - D4 (P1.10) = NSS + * - D5 (P1.11) = RXEN + * + * Console: UART20 (P1.09 TX, P1.08 RX) via SAMD11 CMSIS-DAP bridge → USB CDC + * Flash: RRAM (not traditional NV flash) + * Flash via: SWD through SAMD11 CMSIS-DAP (west flash / openocd) + */ + +/ { + aliases { + lora0 = &lora; + }; + + chosen { + zephyr,settings-partition = &storage_partition; + }; +}; + +/* Disable I2C22 — default pins P1.10/P1.11 (D4/D5) conflict with NSS/RXEN */ +&i2c22 { + status = "disabled"; +}; + +/* SX1262 on spi00 (XIAO SPI bus — SCK/MISO/MOSI already in pinctrl) */ +&spi00 { + cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>; /* D4 = P1.10 NSS */ + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <8000000>; + + /* LoRa control pins */ + reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>; /* D2 = P1.06 */ + busy-gpios = <&gpio1 7 GPIO_ACTIVE_HIGH>; /* D3 = P1.07 */ + dio1-gpios = <&gpio1 5 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* D1 = P1.05 */ + + /* RF switch: RXEN for RX, DIO2 drives TX enable */ + rx-enable-gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>; /* D5 = P1.11 RXEN */ + dio2-tx-enable; + + /* TCXO 1.8V via DIO3 (SX126X_DIO3_TCXO_1V8 = 0x02) */ + dio3-tcxo-voltage = <2>; + tcxo-power-startup-delay-ms = <10>; + + /* RX boosted mode for better sensitivity */ + rx-boosted; + }; +}; + +/* + * RRAM partitions — repartition for ZephCore + * Total RRAM: 1428KB (0x165000) + * + * Default layout: boot(64KB) + slot0(664KB) + slot1(664KB) + storage(36KB) + * New layout (no MCUboot DFU for companion device): + * + * 0x00000 - 0x10000 (64KB) MCUboot (reserved, even if unused) + * 0x10000 - 0x14E000 (1272KB) Application (slot0 — expanded into slot1 space) + * 0x14E000 - 0x150000 (8KB) NVS storage (BLE bonds) — 2 sectors × 4KB + * 0x150000 - 0x165000 (84KB) LittleFS (identity, contacts, channels) + */ +/delete-node/ &slot0_partition; +/delete-node/ &slot1_partition; +/delete-node/ &storage_partition; + +&cpuapp_rram { + partitions { + /* Application — expanded to use slot1 space */ + slot0_partition: partition@10000 { + label = "image-0"; + reg = <0x10000 0x13E000>; /* 1272KB */ + }; + + /* NVS 8KB for BLE bonds + settings (2 × 4KB sectors) */ + storage_partition: partition@14e000 { + label = "storage"; + reg = <0x14E000 0x2000>; + }; + + /* LittleFS 84KB for DataStore */ + lfs_partition: partition@150000 { + label = "lfs"; + reg = <0x150000 0x15000>; + }; + }; +}; + +/* LittleFS auto-mount — standard /lfs mount point */ +#include "../../common/filesystem.dtsi" diff --git a/zephcore/dts/bindings/input/zephcore,input-multi-tap.yaml b/zephcore/dts/bindings/input/zephcore,input-multi-tap.yaml new file mode 100644 index 0000000..1957eb8 --- /dev/null +++ b/zephcore/dts/bindings/input/zephcore,input-multi-tap.yaml @@ -0,0 +1,57 @@ +# Copyright 2025 ZephCore +# SPDX-License-Identifier: Apache-2.0 + +description: | + Input multi-tap pseudo-device. + + Listens for key-press events from a parent input device and counts rapid + taps within a configurable window. When the window expires, emits a key + code based on the number of taps observed. + + The tap-codes array maps tap counts to output key codes: + tap-codes[0] = code emitted for 1 tap + tap-codes[1] = code emitted for 2 taps + ...up to 4 entries. + + If the tap count reaches the length of tap-codes, the code is emitted + immediately (no need to wait for more taps). Otherwise the tap-delay-ms + window must expire before the code is emitted. + + Example (single button with page-next and buzzer-toggle): + user_btn_multitap { + compatible = "zephcore,input-multi-tap"; + input = <&buttons>; + input-codes = ; + tap-codes = ; + tap-delay-ms = <400>; + }; + +compatible: "zephcore,input-multi-tap" + +properties: + input: + type: phandle + description: | + Input device phandle to listen to. If not specified, listens for + input from all devices. + + input-codes: + type: array + required: true + description: | + Array of input event key codes to watch (INPUT_KEY_* or INPUT_BTN_*). + + tap-codes: + type: array + required: true + description: | + Array of output key codes for each tap count (1-based). + Entry 0 = 1 tap, entry 1 = 2 taps, etc. + Maximum 4 entries. + + tap-delay-ms: + type: int + required: true + description: | + Maximum time in milliseconds between taps. After the last tap, + this delay must expire before the tap code is emitted. diff --git a/zephcore/dts/bindings/vendor-prefixes.txt b/zephcore/dts/bindings/vendor-prefixes.txt new file mode 100644 index 0000000..26da973 --- /dev/null +++ b/zephcore/dts/bindings/vendor-prefixes.txt @@ -0,0 +1 @@ +zephcore ZephCore diff --git a/zephcore/helpers/AdvertDataHelpers.cpp b/zephcore/helpers/AdvertDataHelpers.cpp new file mode 100644 index 0000000..4fb1ec8 --- /dev/null +++ b/zephcore/helpers/AdvertDataHelpers.cpp @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore AdvertDataHelpers implementation + */ + +#include "AdvertDataHelpers.h" +#include + +uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[]) +{ + app_data[0] = _type; + int i = 1; + + if (_has_loc) { + app_data[0] |= ADV_LATLON_MASK; + memcpy(&app_data[i], &_lat, 4); i += 4; + memcpy(&app_data[i], &_lon, 4); i += 4; + } + if (_extra1) { + app_data[0] |= ADV_FEAT1_MASK; + memcpy(&app_data[i], &_extra1, 2); i += 2; + } + if (_extra2) { + app_data[0] |= ADV_FEAT2_MASK; + memcpy(&app_data[i], &_extra2, 2); i += 2; + } + if (_name && *_name != 0) { + app_data[0] |= ADV_NAME_MASK; + const char *sp = _name; + while (*sp && i < MAX_ADVERT_DATA_SIZE) { + app_data[i++] = *sp++; + } + } + return i; +} + +AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) +{ + _name[0] = 0; + _lat = _lon = 0; + _valid = false; + _extra1 = _extra2 = 0; + _flags = 0; + + if (app_data_len < 1) return; + _flags = app_data[0]; + + int i = 1; + if (_flags & ADV_LATLON_MASK) { + if (app_data_len < (uint8_t)(i + 8)) return; + memcpy(&_lat, &app_data[i], 4); i += 4; + memcpy(&_lon, &app_data[i], 4); i += 4; + } + if (_flags & ADV_FEAT1_MASK) { + if (app_data_len < (uint8_t)(i + 2)) return; + memcpy(&_extra1, &app_data[i], 2); i += 2; + } + if (_flags & ADV_FEAT2_MASK) { + if (app_data_len < (uint8_t)(i + 2)) return; + memcpy(&_extra2, &app_data[i], 2); i += 2; + } + + if (_flags & ADV_NAME_MASK) { + int nlen = app_data_len - i; + if (nlen > 0) { + memcpy(_name, &app_data[i], nlen); + _name[nlen] = 0; + } + } + _valid = true; +} diff --git a/zephcore/helpers/AdvertDataHelpers.h b/zephcore/helpers/AdvertDataHelpers.h new file mode 100644 index 0000000..a076ca0 --- /dev/null +++ b/zephcore/helpers/AdvertDataHelpers.h @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore AdvertDataHelpers - encode/decode advertisement payloads + */ + +#pragma once + +#include +#include +#include + +#define ADV_TYPE_NONE 0 +#define ADV_TYPE_CHAT 1 +#define ADV_TYPE_REPEATER 2 +#define ADV_TYPE_ROOM 3 +#define ADV_TYPE_SENSOR 4 + +#define ADV_LATLON_MASK 0x10 +#define ADV_FEAT1_MASK 0x20 +#define ADV_FEAT2_MASK 0x40 +#define ADV_NAME_MASK 0x80 + +class AdvertDataBuilder { + uint8_t _type; + bool _has_loc; + const char *_name; + int32_t _lat, _lon; + uint16_t _extra1; + uint16_t _extra2; + +public: + AdvertDataBuilder(uint8_t adv_type) + : _type(adv_type), _has_loc(false), _name(nullptr), _lat(0), _lon(0), _extra1(0), _extra2(0) {} + + AdvertDataBuilder(uint8_t adv_type, const char *name) + : _type(adv_type), _has_loc(false), _name(name), _lat(0), _lon(0), _extra1(0), _extra2(0) {} + + AdvertDataBuilder(uint8_t adv_type, const char *name, double lat, double lon) + : _type(adv_type), _has_loc(true), _name(name), + _lat((int32_t)(lat * 1E6)), _lon((int32_t)(lon * 1E6)), _extra1(0), _extra2(0) {} + + void setFeat1(uint16_t extra) { _extra1 = extra; } + void setFeat2(uint16_t extra) { _extra2 = extra; } + + /** + * Encode the advertisement data to a buffer. + * @param app_data dest array, must be MAX_ADVERT_DATA_SIZE + * @returns the encoded length in bytes + */ + uint8_t encodeTo(uint8_t app_data[]); +}; + +class AdvertDataParser { + uint8_t _flags; + bool _valid; + char _name[MAX_ADVERT_DATA_SIZE]; + int32_t _lat, _lon; + uint16_t _extra1; + uint16_t _extra2; + +public: + AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len); + + bool isValid() const { return _valid; } + uint8_t getType() const { return _flags & 0x0F; } + uint16_t getFeat1() const { return _extra1; } + uint16_t getFeat2() const { return _extra2; } + + bool hasName() const { return _name[0] != 0; } + const char *getName() const { return _name; } + + bool hasLatLon() const { return (_flags & ADV_LATLON_MASK) != 0; } + int32_t getIntLat() const { return _lat; } + int32_t getIntLon() const { return _lon; } + double getLat() const { return ((double)_lat) / 1000000.0; } + double getLon() const { return ((double)_lon) / 1000000.0; } +}; diff --git a/zephcore/helpers/BaseChatMesh.cpp b/zephcore/helpers/BaseChatMesh.cpp new file mode 100644 index 0000000..7ce232f --- /dev/null +++ b/zephcore/helpers/BaseChatMesh.cpp @@ -0,0 +1,945 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore BaseChatMesh implementation + */ + +#include "BaseChatMesh.h" +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_basechat, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); + +#ifndef SERVER_RESPONSE_DELAY +#define SERVER_RESPONSE_DELAY 300 +#endif + +#ifndef TXT_ACK_DELAY +#define TXT_ACK_DELAY 200 +#endif + +void BaseChatMesh::sendFloodScoped(const ContactInfo &recipient, mesh::Packet *pkt, uint32_t delay_millis) +{ + sendFlood(pkt, delay_millis); +} + +void BaseChatMesh::sendFloodScoped(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t delay_millis) +{ + sendFlood(pkt, delay_millis); +} + +mesh::Packet *BaseChatMesh::createSelfAdvert(const char *name) +{ + uint8_t app_data[MAX_ADVERT_DATA_SIZE]; + uint8_t app_data_len; + { + AdvertDataBuilder builder(ADV_TYPE_CHAT, name); + app_data_len = builder.encodeTo(app_data); + } + return createAdvert(self_id, app_data, app_data_len); +} + +mesh::Packet *BaseChatMesh::createSelfAdvert(const char *name, double lat, double lon) +{ + uint8_t app_data[MAX_ADVERT_DATA_SIZE]; + uint8_t app_data_len; + { + AdvertDataBuilder builder(ADV_TYPE_CHAT, name, lat, lon); + app_data_len = builder.encodeTo(app_data); + } + return createAdvert(self_id, app_data, app_data_len); +} + +void BaseChatMesh::sendAckTo(const ContactInfo &dest, uint32_t ack_hash) +{ + if (dest.out_path_len < 0) { + mesh::Packet *ack = createAck(ack_hash); + if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY); + } else { + uint32_t d = TXT_ACK_DELAY; + if (getExtraAckTransmitCount() > 0) { + mesh::Packet *a1 = createMultiAck(ack_hash, 1); + if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d); + d += 300; + } + mesh::Packet *a2 = createAck(ack_hash); + if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d); + } +} + +void BaseChatMesh::bootstrapRTCfromContacts() +{ + uint32_t latest = 0; + for (int i = 0; i < num_contacts; i++) { + if (contacts[i].lastmod > latest) { + latest = contacts[i].lastmod; + } + } + if (latest != 0) { + getRTCClock()->setCurrentTime(latest + 1); + } +} + +ContactInfo *BaseChatMesh::allocateContactSlot() +{ + if (num_contacts < MAX_CONTACTS) { + return &contacts[num_contacts++]; + } else if (shouldOverwriteWhenFull()) { + int oldest_idx = -1; + uint32_t oldest_lastmod = 0xFFFFFFFF; + for (int i = 0; i < num_contacts; i++) { + bool is_favourite = (contacts[i].flags & 0x01) != 0; + if (!is_favourite && contacts[i].lastmod < oldest_lastmod) { + oldest_lastmod = contacts[i].lastmod; + oldest_idx = i; + } + } + if (oldest_idx >= 0) { + onContactOverwrite(contacts[oldest_idx].id.pub_key); + return &contacts[oldest_idx]; + } + } + return nullptr; +} + +void BaseChatMesh::populateContactFromAdvert(ContactInfo &ci, const mesh::Identity &id, + const AdvertDataParser &parser, uint32_t timestamp) +{ + ci = ContactInfo{}; + ci.id = id; + ci.out_path_len = -1; + StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name)); + ci.type = parser.getType(); + if (parser.hasLatLon()) { + ci.gps_lat = parser.getIntLat(); + ci.gps_lon = parser.getIntLon(); + } + ci.last_advert_timestamp = timestamp; + ci.lastmod = getRTCClock()->getCurrentTime(); +} + +void BaseChatMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32_t timestamp, + const uint8_t *app_data, size_t app_data_len) +{ + LOG_INF("onAdvertRecv: timestamp=%u app_data_len=%u", timestamp, (unsigned)app_data_len); + + AdvertDataParser parser(app_data, app_data_len); + if (!(parser.isValid() && parser.hasName())) { + LOG_WRN("onAdvertRecv: invalid parser or no name (valid=%d, hasName=%d)", + parser.isValid(), parser.hasName()); + return; + } + + LOG_INF("onAdvertRecv: valid advert from '%s' type=%d", parser.getName(), parser.getType()); + + ContactInfo *from = nullptr; + for (int i = 0; i < num_contacts; i++) { + if (id.matches(contacts[i].id)) { + from = &contacts[i]; + if (timestamp <= from->last_advert_timestamp) { + return; // replay attack protection + } + break; + } + } + + // Save raw advert packet for share function + int plen; + { + uint8_t save = packet->header; + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_FLOOD; + plen = packet->writeTo(temp_buf); + packet->header = save; + } + + bool is_new = false; + if (from == nullptr) { + LOG_INF("onAdvertRecv: new contact, checking auto-add for type %d", parser.getType()); + if (!shouldAutoAddContactType(parser.getType())) { + LOG_INF("onAdvertRecv: auto-add disabled for type %d, reporting only", parser.getType()); + ContactInfo ci; + populateContactFromAdvert(ci, id, parser, timestamp); + onDiscoveredContact(ci, true, packet->path_len, packet->path); + return; + } + + from = allocateContactSlot(); + if (from == nullptr) { + LOG_WRN("onAdvertRecv: contact table full, cannot allocate slot"); + ContactInfo ci; + populateContactFromAdvert(ci, id, parser, timestamp); + onDiscoveredContact(ci, true, packet->path_len, packet->path); + onContactsFull(); + return; + } + + LOG_INF("onAdvertRecv: allocated slot, num_contacts=%d", num_contacts); + populateContactFromAdvert(*from, id, parser, timestamp); + from->sync_since = 0; + from->shared_secret_valid = false; + is_new = true; + } else { + LOG_INF("onAdvertRecv: existing contact, updating"); + } + + // Update contact + putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen); + StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name)); + from->type = parser.getType(); + if (parser.hasLatLon()) { + from->gps_lat = parser.getIntLat(); + from->gps_lon = parser.getIntLon(); + } + from->last_advert_timestamp = timestamp; + from->lastmod = getRTCClock()->getCurrentTime(); + + LOG_INF("onAdvertRecv: calling onDiscoveredContact is_new=%d", is_new); + onDiscoveredContact(*from, is_new, packet->path_len, packet->path); +} + +int BaseChatMesh::searchPeersByHash(const uint8_t *hash) +{ + int n = 0; + for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) { + if (contacts[i].id.isHashMatch(hash)) { + matching_peer_indexes[n++] = i; + } + } + return n; +} + +void BaseChatMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) +{ + int i = matching_peer_indexes[peer_idx]; + if (i >= 0 && i < num_contacts) { + memcpy(dest_secret, contacts[i].getSharedSecret(self_id), PUB_KEY_SIZE); + } +} + +void BaseChatMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, + const uint8_t *secret, uint8_t *data, size_t len) +{ + LOG_INF("onPeerDataRecv: type=%d sender_idx=%d len=%u", type, sender_idx, (unsigned)len); + int i = matching_peer_indexes[sender_idx]; + if (i < 0 || i >= num_contacts) { + LOG_WRN("onPeerDataRecv: invalid peer index %d (num_contacts=%d)", i, num_contacts); + return; + } + + ContactInfo &from = contacts[i]; + LOG_INF("onPeerDataRecv: from '%s'", from.name); + + if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { + uint32_t timestamp; + memcpy(×tamp, data, 4); + uint8_t flags = data[4] >> 2; + LOG_INF("onPeerDataRecv TXT_MSG: timestamp=%u flags=%d (data[4]=0x%02x) text='%s'", + timestamp, flags, data[4], (const char*)&data[5]); + + data[len] = 0; // null terminate + + if (flags == TXT_TYPE_PLAIN) { + LOG_INF("onPeerDataRecv: flags match TXT_TYPE_PLAIN, calling onMessageRecv"); + from.lastmod = getRTCClock()->getCurrentTime(); + onMessageRecv(from, packet, timestamp, (const char *)&data[5]); + + uint32_t ack_hash; + mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 5 + strlen((char *)&data[5]), + from.id.pub_key, PUB_KEY_SIZE); + + if (packet->isRouteFlood()) { + mesh::Packet *path = createPathReturn(from.id, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_ACK, (uint8_t *)&ack_hash, 4); + if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); + } else { + sendAckTo(from, ack_hash); + } + } else if (flags == TXT_TYPE_CLI_DATA) { + onCommandDataRecv(from, packet, timestamp, (const char *)&data[5]); + + if (packet->isRouteFlood()) { + mesh::Packet *path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, nullptr, 0); + if (path) sendFloodScoped(from, path); + } + } else if (flags == TXT_TYPE_SIGNED_PLAIN) { + if (timestamp > from.sync_since) { + from.sync_since = timestamp; + } + from.lastmod = getRTCClock()->getCurrentTime(); + onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *)&data[9]); + + uint32_t ack_hash; + mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 9 + strlen((char *)&data[9]), + self_id.pub_key, PUB_KEY_SIZE); + + if (packet->isRouteFlood()) { + mesh::Packet *path = createPathReturn(from.id, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_ACK, (uint8_t *)&ack_hash, 4); + if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); + } else { + sendAckTo(from, ack_hash); + } + } + } else if (type == PAYLOAD_TYPE_REQ && len > 4) { + uint32_t sender_timestamp; + memcpy(&sender_timestamp, data, 4); + uint8_t reply_len = onContactRequest(from, sender_timestamp, &data[4], len - 4, temp_buf); + if (reply_len > 0) { + if (packet->isRouteFlood()) { + mesh::Packet *path = createPathReturn(from.id, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len); + if (path) sendFloodScoped(from, path, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len); + if (reply) { + if (from.out_path_len >= 0) { + sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY); + } else { + sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY); + } + } + } + } + } else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) { + LOG_INF("onPeerDataRecv: RESPONSE received, len=%d, calling onContactResponse", len); + onContactResponse(from, data, len); + if (packet->isRouteFlood() && from.out_path_len >= 0) { + handleReturnPathRetry(from, packet->path, packet->path_len); + } + } +} + +bool BaseChatMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t *secret, + uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) +{ + int i = matching_peer_indexes[sender_idx]; + if (i < 0 || i >= num_contacts) { + return false; + } + + ContactInfo &from = contacts[i]; + return onContactPathRecv(from, packet->path, packet->path_len, path, path_len, extra_type, extra, extra_len); +} + +bool BaseChatMesh::onContactPathRecv(ContactInfo &from, uint8_t *in_path, uint8_t in_path_len, + uint8_t *out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) +{ + memcpy(from.out_path, out_path, from.out_path_len = out_path_len); + from.lastmod = getRTCClock()->getCurrentTime(); + + onContactPathUpdated(from); + + if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) { + if (processAck(extra) != nullptr) { + txt_send_timeout = 0; + } + } else if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) { + onContactResponse(from, extra, extra_len); + } + return true; +} + +void BaseChatMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) +{ + LOG_INF("onAckRecv: ack_crc=0x%08x route=%s", ack_crc, + packet->isRouteFlood() ? "flood" : "direct"); + ContactInfo *from; + if ((from = processAck((uint8_t *)&ack_crc)) != nullptr) { + LOG_INF("onAckRecv: ACK processed successfully for '%s'", from->name); + txt_send_timeout = 0; + packet->markDoNotRetransmit(); + + if (packet->isRouteFlood() && from->out_path_len >= 0) { + handleReturnPathRetry(*from, packet->path, packet->path_len); + } + } else { + LOG_INF("onAckRecv: ACK not matched (no pending or wrong crc)"); + } +} + +void BaseChatMesh::handleReturnPathRetry(const ContactInfo &contact, const uint8_t *path, uint8_t path_len) +{ + mesh::Packet *rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, nullptr, 0); + if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); +} + +int BaseChatMesh::searchChannelsByHash(const uint8_t *hash, mesh::GroupChannel dest[], int max_matches) +{ + int n = 0; + for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) { + if (channels[i].channel.hash[0] == hash[0]) { + dest[n++] = channels[i].channel; + } + } + return n; +} + +void BaseChatMesh::onGroupDataRecv(mesh::Packet *packet, uint8_t type, const mesh::GroupChannel &channel, + uint8_t *data, size_t len) +{ + uint8_t txt_type = data[4]; + if (type == PAYLOAD_TYPE_GRP_TXT && len > 5 && (txt_type >> 2) == 0) { + uint32_t timestamp; + memcpy(×tamp, data, 4); + data[len] = 0; + onChannelMessageRecv(channel, packet, timestamp, (const char *)&data[5]); + } +} + +mesh::Packet *BaseChatMesh::composeMsgPacket(const ContactInfo &recipient, uint32_t timestamp, + uint8_t attempt, const char *text, uint32_t &expected_ack) +{ + int text_len = strlen(text); + if (text_len > MAX_TEXT_LEN) return nullptr; + if (attempt > 3 && text_len > MAX_TEXT_LEN - 2) return nullptr; + + uint8_t temp[5 + MAX_TEXT_LEN + 1]; + memcpy(temp, ×tamp, 4); + temp[4] = (attempt & 3); + memcpy(&temp[5], text, text_len + 1); + + mesh::Utils::sha256((uint8_t *)&expected_ack, 4, temp, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE); + + int len = 5 + text_len; + if (attempt > 3) { + temp[len++] = 0; + temp[len++] = attempt; + } + + return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, len); +} + +int BaseChatMesh::sendMessage(const ContactInfo &recipient, uint32_t timestamp, uint8_t attempt, + const char *text, uint32_t &expected_ack, uint32_t &est_timeout) +{ + LOG_INF("sendMessage: to '%s' text='%s' attempt=%d path_len=%d", + recipient.name, text, attempt, recipient.out_path_len); + + mesh::Packet *pkt = composeMsgPacket(recipient, timestamp, attempt, text, expected_ack); + if (pkt == nullptr) { + LOG_WRN("sendMessage: composeMsgPacket failed"); + return MSG_SEND_FAILED; + } + + LOG_INF("sendMessage: packet created, expected_ack=0x%08x raw_len=%d", + expected_ack, pkt->getRawLength()); + + uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); + + int rc; + if (recipient.out_path_len < 0) { + LOG_INF("sendMessage: sending flood"); + sendFloodScoped(recipient, pkt); + txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); + rc = MSG_SEND_SENT_FLOOD; + } else { + LOG_INF("sendMessage: sending direct path_len=%d", recipient.out_path_len); + sendDirect(pkt, recipient.out_path, recipient.out_path_len); + txt_send_timeout = futureMillis(est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len)); + rc = MSG_SEND_SENT_DIRECT; + } + LOG_INF("sendMessage: result=%d est_timeout=%u", rc, est_timeout); + return rc; +} + +int BaseChatMesh::sendCommandData(const ContactInfo &recipient, uint32_t timestamp, uint8_t attempt, + const char *text, uint32_t &est_timeout) +{ + int text_len = strlen(text); + if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED; + + uint8_t temp[5 + MAX_TEXT_LEN + 1]; + memcpy(temp, ×tamp, 4); + temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2); + memcpy(&temp[5], text, text_len + 1); + + mesh::Packet *pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, + recipient.getSharedSecret(self_id), temp, 5 + text_len); + if (pkt == nullptr) return MSG_SEND_FAILED; + + uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); + int rc; + if (recipient.out_path_len < 0) { + sendFloodScoped(recipient, pkt); + txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); + rc = MSG_SEND_SENT_FLOOD; + } else { + sendDirect(pkt, recipient.out_path, recipient.out_path_len); + txt_send_timeout = futureMillis(est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len)); + rc = MSG_SEND_SENT_DIRECT; + } + return rc; +} + +bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel &channel, + const char *sender_name, const char *text, int text_len) +{ + uint8_t temp[5 + MAX_TEXT_LEN + 32]; + memcpy(temp, ×tamp, 4); + temp[4] = 0; // TXT_TYPE_PLAIN + + snprintf((char *)&temp[5], sizeof(temp) - 5, "%s: ", sender_name); + char *ep = strchr((char *)&temp[5], 0); + int prefix_len = ep - (char *)&temp[5]; + + if (text_len + prefix_len > MAX_TEXT_LEN) text_len = MAX_TEXT_LEN - prefix_len; + memcpy(ep, text, text_len); + ep[text_len] = 0; + + mesh::Packet *pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len); + if (pkt) { + sendFloodScoped(channel, pkt); + return true; + } + return false; +} + +bool BaseChatMesh::shareContactZeroHop(const ContactInfo &contact) +{ + int plen = getBlobByKey(contact.id.pub_key, PUB_KEY_SIZE, temp_buf); + if (plen == 0) return false; + + mesh::Packet *packet = obtainNewPacket(); + if (packet == nullptr) return false; + + packet->readFrom(temp_buf, plen); + uint16_t codes[2]; + codes[0] = codes[1] = 0; + sendZeroHop(packet, codes); + return true; +} + +uint8_t BaseChatMesh::exportContact(const ContactInfo &contact, uint8_t dest_buf[]) +{ + return getBlobByKey(contact.id.pub_key, PUB_KEY_SIZE, dest_buf); +} + +bool BaseChatMesh::importContact(const uint8_t src_buf[], uint8_t len) +{ + mesh::Packet *pkt = obtainNewPacket(); + if (pkt) { + if (pkt->readFrom(src_buf, len) && pkt->getPayloadType() == PAYLOAD_TYPE_ADVERT) { + pkt->header |= ROUTE_TYPE_FLOOD; + getTables()->clear(pkt); + _pendingLoopback = pkt; + return true; + } else { + releasePacket(pkt); + } + } + return false; +} + +int BaseChatMesh::sendLogin(const ContactInfo &recipient, const char *password, uint32_t &est_timeout) +{ + mesh::Packet *pkt; + { + int tlen; + uint8_t temp[24]; + uint32_t now = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &now, 4); + if (recipient.type == ADV_TYPE_ROOM) { + memcpy(&temp[4], &recipient.sync_since, 4); + int len = strlen(password); if (len > 15) len = 15; + memcpy(&temp[8], password, len); + tlen = 8 + len; + } else { + int len = strlen(password); if (len > 15) len = 15; + memcpy(&temp[4], password, len); + tlen = 4 + len; + } + + pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, + recipient.getSharedSecret(self_id), temp, tlen); + } + if (pkt) { + uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); + if (recipient.out_path_len < 0) { + sendFloodScoped(recipient, pkt); + est_timeout = calcFloodTimeoutMillisFor(t); + return MSG_SEND_SENT_FLOOD; + } else { + sendDirect(pkt, recipient.out_path, recipient.out_path_len); + est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len); + return MSG_SEND_SENT_DIRECT; + } + } + return MSG_SEND_FAILED; +} + +int BaseChatMesh::sendAnonReq(const ContactInfo &recipient, const uint8_t *data, uint8_t len, + uint32_t &tag, uint32_t &est_timeout) +{ + mesh::Packet *pkt; + { + uint8_t temp[MAX_PACKET_PAYLOAD]; + tag = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &tag, 4); + memcpy(&temp[4], data, len); + + pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, + recipient.getSharedSecret(self_id), temp, 4 + len); + } + if (pkt) { + uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); + if (recipient.out_path_len < 0) { + sendFloodScoped(recipient, pkt); + est_timeout = calcFloodTimeoutMillisFor(t); + return MSG_SEND_SENT_FLOOD; + } else { + sendDirect(pkt, recipient.out_path, recipient.out_path_len); + est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len); + return MSG_SEND_SENT_DIRECT; + } + } + return MSG_SEND_FAILED; +} + +int BaseChatMesh::sendRequest(const ContactInfo &recipient, const uint8_t *req_data, uint8_t data_len, + uint32_t &tag, uint32_t &est_timeout) +{ + if (data_len > MAX_PACKET_PAYLOAD - 16) return MSG_SEND_FAILED; + + mesh::Packet *pkt; + { + uint8_t temp[MAX_PACKET_PAYLOAD]; + tag = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &tag, 4); + memcpy(&temp[4], req_data, data_len); + + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len); + } + if (pkt) { + uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); + if (recipient.out_path_len < 0) { + sendFloodScoped(recipient, pkt); + est_timeout = calcFloodTimeoutMillisFor(t); + return MSG_SEND_SENT_FLOOD; + } else { + sendDirect(pkt, recipient.out_path, recipient.out_path_len); + est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len); + return MSG_SEND_SENT_DIRECT; + } + } + return MSG_SEND_FAILED; +} + +int BaseChatMesh::sendRequest(const ContactInfo &recipient, uint8_t req_type, uint32_t &tag, uint32_t &est_timeout) +{ + mesh::Packet *pkt; + { + uint8_t temp[13]; + tag = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &tag, 4); + temp[4] = req_type; + memset(&temp[5], 0, 4); + getRNG()->random(&temp[9], 4); + + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp)); + } + if (pkt) { + uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); + if (recipient.out_path_len < 0) { + sendFloodScoped(recipient, pkt); + est_timeout = calcFloodTimeoutMillisFor(t); + return MSG_SEND_SENT_FLOOD; + } else { + sendDirect(pkt, recipient.out_path, recipient.out_path_len); + est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len); + return MSG_SEND_SENT_DIRECT; + } + } + return MSG_SEND_FAILED; +} + +void BaseChatMesh::resetPathTo(ContactInfo &recipient) +{ + recipient.out_path_len = -1; +} + +static ContactInfo *table_sort; // pass via global for qsort + +static int cmp_adv_timestamp(const void *a, const void *b) +{ + int a_idx = *((int *)a); + int b_idx = *((int *)b); + if (table_sort[b_idx].last_advert_timestamp > table_sort[a_idx].last_advert_timestamp) return 1; + if (table_sort[b_idx].last_advert_timestamp < table_sort[a_idx].last_advert_timestamp) return -1; + return 0; +} + +void BaseChatMesh::scanRecentContacts(int last_n, ContactVisitor *visitor) +{ + for (int i = 0; i < num_contacts; i++) { + sort_array[i] = i; + } + table_sort = contacts; + qsort(sort_array, num_contacts, sizeof(sort_array[0]), cmp_adv_timestamp); + + if (last_n == 0) { + last_n = num_contacts; + } else { + if (last_n > num_contacts) last_n = num_contacts; + } + for (int i = 0; i < last_n; i++) { + visitor->onContactVisit(contacts[sort_array[i]]); + } +} + +ContactInfo *BaseChatMesh::searchContactsByPrefix(const char *name_prefix) +{ + int len = strlen(name_prefix); + for (int i = 0; i < num_contacts; i++) { + ContactInfo *c = &contacts[i]; + if (memcmp(c->name, name_prefix, len) == 0) return c; + } + return nullptr; +} + +ContactInfo *BaseChatMesh::lookupContactByPubKey(const uint8_t *pub_key, int prefix_len) +{ + for (int i = 0; i < num_contacts; i++) { + ContactInfo *c = &contacts[i]; + if (memcmp(c->id.pub_key, pub_key, prefix_len) == 0) return c; + } + return nullptr; +} + +bool BaseChatMesh::addContact(const ContactInfo &contact) +{ + ContactInfo *dest = allocateContactSlot(); + if (dest) { + *dest = contact; + dest->shared_secret_valid = false; + return true; + } + return false; +} + +bool BaseChatMesh::removeContact(ContactInfo &contact) +{ + int idx = 0; + while (idx < num_contacts && !contacts[idx].id.matches(contact.id)) { + idx++; + } + if (idx >= num_contacts) return false; + + num_contacts--; + while (idx < num_contacts) { + contacts[idx] = contacts[idx + 1]; + idx++; + } + return true; +} + +ChannelDetails *BaseChatMesh::addChannel(const char *name, const uint8_t *psk, size_t psk_len) +{ + if (num_channels < MAX_GROUP_CHANNELS) { + ChannelDetails *dest = &channels[num_channels]; + + memset(dest->channel.secret, 0, sizeof(dest->channel.secret)); + if (psk_len == 32 || psk_len == 16) { + memcpy(dest->channel.secret, psk, psk_len); + mesh::Utils::sha256(dest->channel.hash, sizeof(dest->channel.hash), dest->channel.secret, psk_len); + StrHelper::strncpy(dest->name, name, sizeof(dest->name)); + num_channels++; + return dest; + } + } + return nullptr; +} + +bool BaseChatMesh::getChannel(int idx, ChannelDetails &dest) +{ + if (idx >= 0 && idx < MAX_GROUP_CHANNELS) { + dest = channels[idx]; + return true; + } + return false; +} + +bool BaseChatMesh::setChannel(int idx, const ChannelDetails &src) +{ + static uint8_t zeroes[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }; + + if (idx >= 0 && idx < MAX_GROUP_CHANNELS) { + channels[idx] = src; + if (memcmp(&src.channel.secret[16], zeroes, 16) == 0) { + mesh::Utils::sha256(channels[idx].channel.hash, sizeof(channels[idx].channel.hash), + src.channel.secret, 16); + } else { + mesh::Utils::sha256(channels[idx].channel.hash, sizeof(channels[idx].channel.hash), + src.channel.secret, 32); + } + return true; + } + return false; +} + +int BaseChatMesh::findChannelIdx(const mesh::GroupChannel &ch) +{ + for (int i = 0; i < MAX_GROUP_CHANNELS; i++) { + if (memcmp(ch.secret, channels[i].channel.secret, sizeof(ch.secret)) == 0) return i; + } + return -1; +} + +bool BaseChatMesh::getContactByIdx(uint32_t idx, ContactInfo &contact) +{ + if (idx >= (uint32_t)num_contacts) return false; + contact = contacts[idx]; + return true; +} + +ContactsIterator BaseChatMesh::startContactsIterator() +{ + return ContactsIterator(); +} + +bool ContactsIterator::hasNext(const BaseChatMesh *mesh, ContactInfo &dest) +{ + if (next_idx >= mesh->getNumContacts()) return false; + dest = mesh->contacts[next_idx++]; + return true; +} + +/* Connection management for room servers */ +bool BaseChatMesh::startConnection(const ContactInfo &contact, uint16_t keep_alive_secs) +{ + int use_idx = -1; + for (int i = 0; i < MAX_CONNECTIONS; i++) { + if (connections[i].keep_alive_millis == 0) { // free slot + use_idx = i; + } else if (connections[i].server_id.matches(contact.id)) { // already in table + use_idx = i; + break; + } + } + if (use_idx < 0) return false; // table is full + + connections[use_idx].server_id = contact.id; + uint32_t interval = connections[use_idx].keep_alive_millis = (uint32_t)keep_alive_secs * 1000; + connections[use_idx].next_ping = futureMillis(interval); + connections[use_idx].expected_ack = 0; + connections[use_idx].last_activity = getRTCClock()->getCurrentTime(); + return true; +} + +void BaseChatMesh::stopConnection(const uint8_t *pub_key) +{ + for (int i = 0; i < MAX_CONNECTIONS; i++) { + if (connections[i].server_id.matches(pub_key)) { + connections[i].keep_alive_millis = 0; // mark slot as free + connections[i].next_ping = 0; + connections[i].expected_ack = 0; + connections[i].last_activity = 0; + break; + } + } +} + +bool BaseChatMesh::hasConnectionTo(const uint8_t *pub_key) +{ + for (int i = 0; i < MAX_CONNECTIONS; i++) { + if (connections[i].keep_alive_millis > 0 && connections[i].server_id.matches(pub_key)) return true; + } + return false; +} + +void BaseChatMesh::markConnectionActive(const ContactInfo &contact) +{ + for (int i = 0; i < MAX_CONNECTIONS; i++) { + if (connections[i].keep_alive_millis > 0 && connections[i].server_id.matches(contact.id)) { + connections[i].last_activity = getRTCClock()->getCurrentTime(); + // schedule next keep-alive + connections[i].next_ping = futureMillis(connections[i].keep_alive_millis); + return; + } + } +} + +ContactInfo *BaseChatMesh::checkConnectionsAck(const uint8_t *data) +{ + for (int i = 0; i < MAX_CONNECTIONS; i++) { + if (connections[i].keep_alive_millis > 0 && memcmp(&connections[i].expected_ack, data, 4) == 0) { + // got an ack for our keep_alive request + connections[i].expected_ack = 0; + connections[i].last_activity = getRTCClock()->getCurrentTime(); + connections[i].next_ping = futureMillis(connections[i].keep_alive_millis); + return lookupContactByPubKey(connections[i].server_id.pub_key, PUB_KEY_SIZE); + } + } + return nullptr; +} + +void BaseChatMesh::checkConnections() +{ + // scan connections[] table, send KEEP_ALIVE requests + for (int i = 0; i < MAX_CONNECTIONS; i++) { + if (connections[i].keep_alive_millis == 0) continue; // unused slot + + uint32_t now = getRTCClock()->getCurrentTime(); + uint32_t expire_secs = (connections[i].keep_alive_millis / 1000) * 5 / 2; // 2.5x keep_alive + if (now >= connections[i].last_activity + expire_secs) { + // connection timed out + connections[i].keep_alive_millis = 0; + connections[i].next_ping = 0; + connections[i].expected_ack = 0; + connections[i].last_activity = 0; + continue; + } + + if (millisHasNowPassed(connections[i].next_ping)) { + ContactInfo *contact = lookupContactByPubKey(connections[i].server_id.pub_key, PUB_KEY_SIZE); + if (contact == nullptr) { + LOG_WRN("checkConnections: keep_alive contact not found"); + continue; + } + if (contact->out_path_len < 0) { + LOG_WRN("checkConnections: keep_alive contact has no out_path"); + continue; + } + + // Build keepalive request manually (9 bytes, matching Arduino wire format) + uint8_t data[9]; + uint32_t ts = getRTCClock()->getCurrentTimeUnique(); + memcpy(data, &ts, 4); + data[4] = REQ_TYPE_KEEP_ALIVE; + memcpy(&data[5], &contact->sync_since, 4); + + // calc expected ACK reply (SHA256 of data + self pubkey, first 4 bytes) + mesh::Utils::sha256((uint8_t *)&connections[i].expected_ack, 4, + data, 9, self_id.pub_key, PUB_KEY_SIZE); + + auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, + contact->getSharedSecret(self_id), data, 9); + if (pkt) { + sendDirect(pkt, contact->out_path, contact->out_path_len); + } + + // schedule next KEEP_ALIVE + connections[i].next_ping = futureMillis(connections[i].keep_alive_millis); + } + } +} + +void BaseChatMesh::loop() +{ + Mesh::loop(); + + if (txt_send_timeout && millisHasNowPassed(txt_send_timeout)) { + onSendTimeout(); + txt_send_timeout = 0; + } + + if (_pendingLoopback) { + onRecvPacket(_pendingLoopback); + releasePacket(_pendingLoopback); + _pendingLoopback = nullptr; + } + + checkConnections(); +} diff --git a/zephcore/helpers/BaseChatMesh.h b/zephcore/helpers/BaseChatMesh.h new file mode 100644 index 0000000..2536cfa --- /dev/null +++ b/zephcore/helpers/BaseChatMesh.h @@ -0,0 +1,201 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore BaseChatMesh - base class for chat-style mesh clients + */ + +#pragma once + +#include +#include "AdvertDataHelpers.h" +#include "TxtDataHelpers.h" +#include "ContactInfo.h" +#include "ChannelDetails.h" + +#define MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE) + +#ifdef CONFIG_ZEPHCORE_MAX_CONTACTS +#define MAX_CONTACTS CONFIG_ZEPHCORE_MAX_CONTACTS +#else +#define MAX_CONTACTS 32 +#endif + +#ifdef CONFIG_ZEPHCORE_MAX_CHANNELS +#define MAX_GROUP_CHANNELS CONFIG_ZEPHCORE_MAX_CHANNELS +#else +#define MAX_GROUP_CHANNELS 8 +#endif + +#define MAX_SEARCH_RESULTS 8 + +#ifdef CONFIG_ZEPHCORE_MAX_CONNECTIONS +#define MAX_CONNECTIONS CONFIG_ZEPHCORE_MAX_CONNECTIONS +#else +#define MAX_CONNECTIONS 16 +#endif + +#define MSG_SEND_FAILED 0 +#define MSG_SEND_SENT_FLOOD 1 +#define MSG_SEND_SENT_DIRECT 2 + +#define REQ_TYPE_GET_STATUS 0x01 +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY 0x03 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 // alias + +#define RESP_SERVER_LOGIN_OK 0 + +/* Telemetry permissions */ +#define TELEM_PERM_BASE 0x01 +#define TELEM_PERM_LOCATION 0x02 +#define TELEM_PERM_ENVIRONMENT 0x04 + +/* Connection info for room server keep-alive */ +struct ConnectionInfo { + mesh::Identity server_id; + unsigned long next_ping; + uint32_t last_activity; + uint32_t keep_alive_millis; + uint32_t expected_ack; +}; + +class BaseChatMesh; + +class ContactVisitor { +public: + virtual void onContactVisit(const ContactInfo &contact) = 0; +}; + +class ContactsIterator { + int next_idx = 0; +public: + bool hasNext(const BaseChatMesh *mesh, ContactInfo &dest); +}; + +/** + * Abstract Mesh class for common 'chat' client functionality. + * Handles contact management, message encryption/decryption, ACK handling. + */ +class BaseChatMesh : public mesh::Mesh { + friend class ContactsIterator; + + ContactInfo contacts[MAX_CONTACTS]; + int num_contacts; + int sort_array[MAX_CONTACTS]; + int matching_peer_indexes[MAX_SEARCH_RESULTS]; + unsigned long txt_send_timeout; + + ChannelDetails channels[MAX_GROUP_CHANNELS]; + int num_channels; + + ConnectionInfo connections[MAX_CONNECTIONS]; + + mesh::Packet *_pendingLoopback; + uint8_t temp_buf[MAX_TRANS_UNIT]; + + mesh::Packet *composeMsgPacket(const ContactInfo &recipient, uint32_t timestamp, uint8_t attempt, + const char *text, uint32_t &expected_ack); + void sendAckTo(const ContactInfo &dest, uint32_t ack_hash); + +protected: + BaseChatMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng, mesh::RTCClock &rtc, + mesh::PacketManager &mgr, mesh::MeshTables &tables) + : mesh::Mesh(radio, ms, rng, rtc, mgr, tables) + { + num_contacts = 0; + memset(channels, 0, sizeof(channels)); + num_channels = 0; + txt_send_timeout = 0; + _pendingLoopback = nullptr; + } + + void bootstrapRTCfromContacts(); + void resetContacts() { num_contacts = 0; } + void populateContactFromAdvert(ContactInfo &ci, const mesh::Identity &id, const AdvertDataParser &parser, uint32_t timestamp); + ContactInfo *allocateContactSlot(); + + // UI concepts for subclasses to implement + virtual bool isAutoAddEnabled() const { return true; } + virtual bool shouldAutoAddContactType(uint8_t type) const { return true; } + virtual void onContactsFull() {} + virtual bool shouldOverwriteWhenFull() const { return false; } + virtual void onContactOverwrite(const uint8_t *pub_key) {} + virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t *path) = 0; + virtual ContactInfo *processAck(const uint8_t *data) = 0; + virtual void onContactPathUpdated(const ContactInfo &contact) = 0; + virtual bool onContactPathRecv(ContactInfo &from, uint8_t *in_path, uint8_t in_path_len, + uint8_t *out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len); + virtual void onMessageRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) = 0; + virtual void onCommandDataRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) = 0; + virtual void onSignedMessageRecv(const ContactInfo &contact, mesh::Packet *pkt, uint32_t sender_timestamp, + const uint8_t *sender_prefix, const char *text) = 0; + virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0; + virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0; + virtual void onSendTimeout() = 0; + virtual void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, const char *text) = 0; + virtual uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, + const uint8_t *data, uint8_t len, uint8_t *reply) = 0; + virtual void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) = 0; + virtual void handleReturnPathRetry(const ContactInfo &contact, const uint8_t *path, uint8_t path_len); + + virtual void sendFloodScoped(const ContactInfo &recipient, mesh::Packet *pkt, uint32_t delay_millis = 0); + virtual void sendFloodScoped(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t delay_millis = 0); + + // Storage concepts for subclasses to override + virtual int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; } + virtual bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) { return false; } + + // Mesh overrides + void onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32_t timestamp, + const uint8_t *app_data, size_t app_data_len) override; + int searchPeersByHash(const uint8_t *hash) override; + void getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) override; + void onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret, + uint8_t *data, size_t len) override; + bool onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t *secret, uint8_t *path, + uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) override; + void onAckRecv(mesh::Packet *packet, uint32_t ack_crc) override; + int searchChannelsByHash(const uint8_t *hash, mesh::GroupChannel channels[], int max_matches) override; + void onGroupDataRecv(mesh::Packet *packet, uint8_t type, const mesh::GroupChannel &channel, + uint8_t *data, size_t len) override; + +public: + mesh::Packet *createSelfAdvert(const char *name); + mesh::Packet *createSelfAdvert(const char *name, double lat, double lon); + int sendMessage(const ContactInfo &recipient, uint32_t timestamp, uint8_t attempt, const char *text, + uint32_t &expected_ack, uint32_t &est_timeout); + int sendCommandData(const ContactInfo &recipient, uint32_t timestamp, uint8_t attempt, const char *text, + uint32_t &est_timeout); + bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel &channel, const char *sender_name, + const char *text, int text_len); + int sendLogin(const ContactInfo &recipient, const char *password, uint32_t &est_timeout); + int sendAnonReq(const ContactInfo &recipient, const uint8_t *data, uint8_t len, uint32_t &tag, uint32_t &est_timeout); + int sendRequest(const ContactInfo &recipient, uint8_t req_type, uint32_t &tag, uint32_t &est_timeout); + int sendRequest(const ContactInfo &recipient, const uint8_t *req_data, uint8_t data_len, uint32_t &tag, uint32_t &est_timeout); + bool shareContactZeroHop(const ContactInfo &contact); + uint8_t exportContact(const ContactInfo &contact, uint8_t dest_buf[]); + bool importContact(const uint8_t src_buf[], uint8_t len); + void resetPathTo(ContactInfo &recipient); + void scanRecentContacts(int last_n, ContactVisitor *visitor); + ContactInfo *searchContactsByPrefix(const char *name_prefix); + ContactInfo *lookupContactByPubKey(const uint8_t *pub_key, int prefix_len); + bool removeContact(ContactInfo &contact); + bool addContact(const ContactInfo &contact); + int getNumContacts() const { return num_contacts; } + bool getContactByIdx(uint32_t idx, ContactInfo &contact); + ContactsIterator startContactsIterator(); + ChannelDetails *addChannel(const char *name, const uint8_t *psk, size_t psk_len); + bool getChannel(int idx, ChannelDetails &dest); + bool setChannel(int idx, const ChannelDetails &src); + int findChannelIdx(const mesh::GroupChannel &ch); + int getNumChannels() const { return num_channels; } + + /* Connection management for room servers */ + bool startConnection(const ContactInfo &contact, uint16_t keep_alive_secs); + void stopConnection(const uint8_t *pub_key); + bool hasConnectionTo(const uint8_t *pub_key); + void markConnectionActive(const ContactInfo &contact); + ContactInfo *checkConnectionsAck(const uint8_t *data); + void checkConnections(); + + void loop(); +}; diff --git a/zephcore/helpers/ChannelDetails.h b/zephcore/helpers/ChannelDetails.h new file mode 100644 index 0000000..b2950eb --- /dev/null +++ b/zephcore/helpers/ChannelDetails.h @@ -0,0 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ChannelDetails - group channel info + */ + +#pragma once + +#include +#include + +struct ChannelDetails { + mesh::GroupChannel channel; + char name[32]; +}; diff --git a/zephcore/helpers/ClientACL.cpp b/zephcore/helpers/ClientACL.cpp new file mode 100644 index 0000000..92c73df --- /dev/null +++ b/zephcore/helpers/ClientACL.cpp @@ -0,0 +1,164 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ClientACL - Access Control List for repeater clients + */ + +#include "ClientACL.h" +#include +#include + +LOG_MODULE_REGISTER(zephcore_acl, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); + +void ClientACL::load(const char* path, const mesh::LocalIdentity& self_id) { + _path = path; + num_clients = 0; + + struct fs_file_t file; + fs_file_t_init(&file); + + if (fs_open(&file, path, FS_O_READ) < 0) { + LOG_DBG("No ACL file at %s", path); + return; + } + + bool full = false; + while (!full) { + ClientInfo c; + uint8_t pub_key[32]; + uint8_t unused[2]; + + c.clear(); + + bool success = (fs_read(&file, pub_key, 32) == 32); + success = success && (fs_read(&file, &c.permissions, 1) == 1); + success = success && (fs_read(&file, &c.extra.room.sync_since, 4) == 4); + success = success && (fs_read(&file, unused, 2) == 2); + success = success && (fs_read(&file, &c.out_path_len, 1) == 1); + success = success && (fs_read(&file, c.out_path, 64) == 64); + success = success && (fs_read(&file, c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); + + if (!success) break; // EOF + + c.id = mesh::Identity(pub_key); + self_id.calcSharedSecret(c.shared_secret, pub_key); // recalculate in case key changed + + if (num_clients < MAX_CLIENTS) { + clients[num_clients++] = c; + } else { + full = true; + } + } + fs_close(&file); + LOG_INF("Loaded %d clients from %s", num_clients, path); +} + +void ClientACL::save(const char* path, bool (*filter)(ClientInfo*)) { + _path = path; + + // Remove old file first (LittleFS doesn't overwrite well) + fs_unlink(path); + + struct fs_file_t file; + fs_file_t_init(&file); + + if (fs_open(&file, path, FS_O_CREATE | FS_O_WRITE) < 0) { + LOG_ERR("Failed to open %s for write", path); + return; + } + + uint8_t unused[2] = {0, 0}; + int saved = 0; + + for (int i = 0; i < num_clients; i++) { + auto c = &clients[i]; + if (c->permissions == 0 || (filter && !filter(c))) continue; // skip deleted or filtered + + bool success = (fs_write(&file, c->id.pub_key, 32) == 32); + success = success && (fs_write(&file, &c->permissions, 1) == 1); + success = success && (fs_write(&file, &c->extra.room.sync_since, 4) == 4); + success = success && (fs_write(&file, unused, 2) == 2); + success = success && (fs_write(&file, &c->out_path_len, 1) == 1); + success = success && (fs_write(&file, c->out_path, 64) == 64); + success = success && (fs_write(&file, c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); + + if (!success) { + LOG_ERR("Write failed at client %d", i); + break; + } + saved++; + } + fs_close(&file); + LOG_INF("Saved %d clients to %s", saved, path); +} + +bool ClientACL::clear() { + if (_path) { + fs_unlink(_path); + } + for (int i = 0; i < MAX_CLIENTS; i++) { + clients[i].clear(); + } + num_clients = 0; + return true; +} + +ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) { + for (int i = 0; i < num_clients; i++) { + if (memcmp(pubkey, clients[i].id.pub_key, key_len) == 0) { + return &clients[i]; // found + } + } + return nullptr; // not found +} + +ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) { + uint32_t min_time = 0xFFFFFFFF; + ClientInfo* oldest = &clients[MAX_CLIENTS - 1]; + + for (int i = 0; i < num_clients; i++) { + if (id.matches(clients[i].id)) { + return &clients[i]; // already known + } + if (!clients[i].isAdmin() && clients[i].last_activity < min_time) { + oldest = &clients[i]; + min_time = oldest->last_activity; + } + } + + ClientInfo* c; + if (num_clients < MAX_CLIENTS) { + c = &clients[num_clients++]; + } else { + c = oldest; // evict least active contact + } + c->clear(); + c->permissions = init_perms; + c->id = id; + c->out_path_len = -1; // initially out_path is unknown + return c; +} + +bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms) { + ClientInfo* c; + if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { + // guest role is not persisted in contacts + c = getClient(pubkey, key_len); + if (c == nullptr) return false; // partial pubkey not found + + num_clients--; // delete from contacts[] + int i = c - clients; + while (i < num_clients) { + clients[i] = clients[i + 1]; + i++; + } + } else { + if (key_len < PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying + + mesh::Identity id(pubkey); + c = putClient(id, 0); + + c->permissions = perms; // update their permissions + self_id.calcSharedSecret(c->shared_secret, pubkey); + } + return true; +} diff --git a/zephcore/helpers/ClientACL.h b/zephcore/helpers/ClientACL.h new file mode 100644 index 0000000..bcc80c0 --- /dev/null +++ b/zephcore/helpers/ClientACL.h @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ClientACL - Access Control List for repeater clients + */ + +#pragma once + +#include +#include + +#define PERM_ACL_ROLE_MASK 3 // lower 2 bits +#define PERM_ACL_GUEST 0 +#define PERM_ACL_READ_ONLY 1 +#define PERM_ACL_READ_WRITE 2 +#define PERM_ACL_ADMIN 3 + +struct ClientInfo { + mesh::Identity id; + uint8_t permissions; + int8_t out_path_len; + uint8_t out_path[MAX_PATH_SIZE]; + uint8_t shared_secret[PUB_KEY_SIZE]; + uint32_t last_timestamp; // by THEIR clock (transient) + uint32_t last_activity; // by OUR clock (transient) + union { + struct { + uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock) + uint32_t pending_ack; + uint32_t push_post_timestamp; + unsigned long ack_timeout; + uint8_t push_failures; + } room; + } extra; + + bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; } + + /* Clear all fields properly */ + void clear() { + id = mesh::Identity(); + permissions = 0; + out_path_len = 0; + memset(out_path, 0, sizeof(out_path)); + memset(shared_secret, 0, sizeof(shared_secret)); + last_timestamp = 0; + last_activity = 0; + memset(&extra, 0, sizeof(extra)); + } +}; + +#ifndef MAX_CLIENTS + #ifdef CONFIG_ZEPHCORE_MAX_CLIENTS + #define MAX_CLIENTS CONFIG_ZEPHCORE_MAX_CLIENTS + #else + #define MAX_CLIENTS 32 + #endif +#endif + +class ClientACL { + const char* _path; + ClientInfo clients[MAX_CLIENTS]; + int num_clients; + +public: + ClientACL() : _path(nullptr), num_clients(0) { + for (int i = 0; i < MAX_CLIENTS; i++) { + clients[i].clear(); + } + } + + void load(const char* path, const mesh::LocalIdentity& self_id); + void save(const char* path, bool (*filter)(ClientInfo*) = nullptr); + bool clear(); + + ClientInfo* getClient(const uint8_t* pubkey, int key_len); + ClientInfo* putClient(const mesh::Identity& id, uint8_t init_perms); + bool applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms); + + int getNumClients() const { return num_clients; } + ClientInfo* getClientByIdx(int idx) { return &clients[idx]; } +}; diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp new file mode 100644 index 0000000..a6eb81f --- /dev/null +++ b/zephcore/helpers/CommonCLI.cpp @@ -0,0 +1,700 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * CommonCLI - Common CLI command handlers for repeaters + */ + +#include "CommonCLI.h" +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(zephcore_cli, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); + +// Helper: robust atoi +static uint32_t _atoi(const char* sp) { + uint32_t n = 0; + while (*sp && *sp >= '0' && *sp <= '9') { + n *= 10; + n += (*sp++ - '0'); + } + return n; +} + +static bool isValidName(const char* n) { + while (*n) { + if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || + *n == ',' || *n == '?' || *n == '*') return false; + n++; + } + return true; +} + +// Constrain helper +template +static T constrain(T value, T min_val, T max_val) { + if (value < min_val) return min_val; + if (value > max_val) return max_val; + return value; +} + +void CommonCLI::loadPrefs(const char* path) { + struct fs_file_t file; + fs_file_t_init(&file); + + if (fs_open(&file, path, FS_O_READ) < 0) { + LOG_DBG("No prefs file at %s, using defaults", path); + return; + } + + uint8_t pad[8]; + + fs_read(&file, &_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0 + fs_read(&file, &_prefs->node_name, sizeof(_prefs->node_name)); // 4 + fs_read(&file, pad, 4); // 36 + fs_read(&file, &_prefs->node_lat, sizeof(_prefs->node_lat)); // 40 + fs_read(&file, &_prefs->node_lon, sizeof(_prefs->node_lon)); // 48 + fs_read(&file, &_prefs->password[0], sizeof(_prefs->password)); // 56 + fs_read(&file, &_prefs->freq, sizeof(_prefs->freq)); // 72 + fs_read(&file, &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76 + fs_read(&file, &_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77 + fs_read(&file, &_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78 + fs_read(&file, pad, 1); // 79 + fs_read(&file, &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80 + fs_read(&file, &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 + fs_read(&file, &_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 + fs_read(&file, &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 + fs_read(&file, pad, 4); // 108 + fs_read(&file, &_prefs->sf, sizeof(_prefs->sf)); // 112 + fs_read(&file, &_prefs->cr, sizeof(_prefs->cr)); // 113 + fs_read(&file, &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 + fs_read(&file, &_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 + fs_read(&file, &_prefs->bw, sizeof(_prefs->bw)); // 116 + fs_read(&file, &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 + fs_read(&file, pad, 3); // 121 + fs_read(&file, &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 + fs_read(&file, &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 + fs_read(&file, &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 + fs_read(&file, pad, 1); // skip bridge_enabled // 127 + fs_read(&file, pad, 2); // skip bridge_delay // 128 + fs_read(&file, pad, 1); // skip bridge_pkt_src // 130 + fs_read(&file, pad, 4); // skip bridge_baud // 131 + fs_read(&file, pad, 1); // skip bridge_channel // 135 + fs_read(&file, pad, 16); // skip bridge_secret // 136 + fs_read(&file, &_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 + fs_read(&file, pad, 3); // 153 + fs_read(&file, &_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 + fs_read(&file, &_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 + fs_read(&file, &_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 + fs_read(&file, &_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 + fs_read(&file, &_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 + fs_read(&file, _prefs->owner_info, sizeof(_prefs->owner_info)); // 170 + fs_read(&file, &_prefs->rx_boost, sizeof(_prefs->rx_boost)); // 290 + + fs_close(&file); + + // Sanitise bad pref values + _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0.0f, 20.0f); + _prefs->tx_delay_factor = constrain(_prefs->tx_delay_factor, 0.0f, 2.0f); + _prefs->direct_tx_delay_factor = constrain(_prefs->direct_tx_delay_factor, 0.0f, 2.0f); + /* Migrate old AF multiplier (0-9) to duty cycle percentage (0-99) */ + if (_prefs->airtime_factor > 0.0f && _prefs->airtime_factor <= 9.0f) { + _prefs->airtime_factor *= 10.0f; + } + _prefs->airtime_factor = constrain(_prefs->airtime_factor, 0.0f, 99.0f); + _prefs->freq = constrain(_prefs->freq, 400.0f, 2500.0f); + _prefs->bw = constrain(_prefs->bw, 7.8f, 500.0f); + _prefs->sf = constrain(_prefs->sf, (uint8_t)5, (uint8_t)12); + _prefs->cr = constrain(_prefs->cr, (uint8_t)5, (uint8_t)8); + _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, (uint8_t)1, (uint8_t)30); + _prefs->multi_acks = constrain(_prefs->multi_acks, (uint8_t)0, (uint8_t)1); + _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); + _prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, (uint8_t)0, (uint8_t)1); + _prefs->gps_enabled = constrain(_prefs->gps_enabled, (uint8_t)0, (uint8_t)1); + _prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, (uint8_t)0, (uint8_t)2); + _prefs->rx_boost = constrain(_prefs->rx_boost, (uint8_t)0, (uint8_t)1); + + LOG_INF("Loaded prefs from %s", path); +} + +void CommonCLI::savePrefs(const char* path) { + // Remove old file first + fs_unlink(path); + + struct fs_file_t file; + fs_file_t_init(&file); + + if (fs_open(&file, path, FS_O_CREATE | FS_O_WRITE) < 0) { + LOG_ERR("Failed to open %s for write", path); + return; + } + + uint8_t pad[16]; + memset(pad, 0, sizeof(pad)); + + fs_write(&file, &_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); + fs_write(&file, &_prefs->node_name, sizeof(_prefs->node_name)); + fs_write(&file, pad, 4); + fs_write(&file, &_prefs->node_lat, sizeof(_prefs->node_lat)); + fs_write(&file, &_prefs->node_lon, sizeof(_prefs->node_lon)); + fs_write(&file, &_prefs->password[0], sizeof(_prefs->password)); + fs_write(&file, &_prefs->freq, sizeof(_prefs->freq)); + fs_write(&file, &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); + fs_write(&file, &_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); + fs_write(&file, &_prefs->advert_interval, sizeof(_prefs->advert_interval)); + fs_write(&file, pad, 1); + fs_write(&file, &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); + fs_write(&file, &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); + fs_write(&file, &_prefs->guest_password[0], sizeof(_prefs->guest_password)); + fs_write(&file, &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); + fs_write(&file, pad, 4); + fs_write(&file, &_prefs->sf, sizeof(_prefs->sf)); + fs_write(&file, &_prefs->cr, sizeof(_prefs->cr)); + fs_write(&file, &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); + fs_write(&file, &_prefs->multi_acks, sizeof(_prefs->multi_acks)); + fs_write(&file, &_prefs->bw, sizeof(_prefs->bw)); + fs_write(&file, &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); + fs_write(&file, pad, 3); + fs_write(&file, &_prefs->flood_max, sizeof(_prefs->flood_max)); + fs_write(&file, &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); + fs_write(&file, &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); + fs_write(&file, pad, 1); // bridge_enabled + fs_write(&file, pad, 2); // bridge_delay + fs_write(&file, pad, 1); // bridge_pkt_src + fs_write(&file, pad, 4); // bridge_baud + fs_write(&file, pad, 1); // bridge_channel + fs_write(&file, pad, 16); // bridge_secret + fs_write(&file, &_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); + fs_write(&file, pad, 3); + fs_write(&file, &_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); + fs_write(&file, &_prefs->gps_interval, sizeof(_prefs->gps_interval)); + fs_write(&file, &_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); + fs_write(&file, &_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); + fs_write(&file, &_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); + fs_write(&file, _prefs->owner_info, sizeof(_prefs->owner_info)); + fs_write(&file, &_prefs->rx_boost, sizeof(_prefs->rx_boost)); + + fs_close(&file); + LOG_INF("Saved prefs to %s", path); +} + +#define MIN_LOCAL_ADVERT_INTERVAL 60 + +void CommonCLI::savePrefs() { + if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) { + _prefs->advert_interval = 0; // turn off, now that device has been manually configured + } + _callbacks->savePrefs(); +} + +uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { + if (_prefs->advert_loc_policy == ADVERT_LOC_NONE) { + AdvertDataBuilder builder(node_type, _prefs->node_name); + return builder.encodeTo(app_data); + } else if (_prefs->advert_loc_policy == ADVERT_LOC_SHARE) { + AdvertDataBuilder builder(node_type, _prefs->node_name, + _callbacks->getNodeLat(), _callbacks->getNodeLon()); + return builder.encodeTo(app_data); + } else { + AdvertDataBuilder builder(node_type, _prefs->node_name, + _prefs->node_lat, _prefs->node_lon); + return builder.encodeTo(app_data); + } +} + +void CommonCLI::rebootWorkHandler(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + CommonCLI *self = CONTAINER_OF(dwork, CommonCLI, _reboot_work); + + switch (self->_pending_reboot) { + case REBOOT_DFU: + static_cast(self->_board)->rebootToBootloader(); + break; + case REBOOT_OTA: + /* reply already sent; startOTAUpdate will reset */ + char dummy[80]; + self->_board->startOTAUpdate(self->_prefs->node_name, dummy); + break; + case REBOOT_NORMAL: + default: + self->_board->reboot(); + break; + } +} + +void CommonCLI::scheduleReboot(uint8_t type) +{ + _pending_reboot = type; + /* 2 second delay - enough for LoRa reply to be transmitted */ + k_work_schedule(&_reboot_work, K_SECONDS(2)); +} + +void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) { + if (strcmp(command, "start dfu") == 0) { + /* Reboot into UF2 mass storage bootloader for firmware update */ + strcpy(reply, "OK - rebooting to UF2 DFU"); + scheduleReboot(REBOOT_DFU); + } else if (memcmp(command, "start ota", 9) == 0) { + /* Reboot into Adafruit BLE OTA DFU mode */ + strcpy(reply, "OK - rebooting to BLE OTA DFU"); + scheduleReboot(REBOOT_OTA); + } else if (memcmp(command, "reboot", 6) == 0) { + strcpy(reply, "OK - rebooting"); + scheduleReboot(REBOOT_NORMAL); + } else if (memcmp(command, "clkreboot", 9) == 0) { + getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm + _board->reboot(); + } else if (memcmp(command, "advert", 6) == 0) { + _callbacks->sendSelfAdvertisement(1500, true); + strcpy(reply, "OK - Advert sent"); + } else if (memcmp(command, "clock sync", 10) == 0) { + uint32_t curr = getRTCClock()->getCurrentTime(); + if (sender_timestamp > curr) { + getRTCClock()->setCurrentTime(sender_timestamp + 1); + uint32_t now = getRTCClock()->getCurrentTime(); + // Simple time formatting + snprintf(reply, CLI_REPLY_SIZE, "OK - clock set to %u", now); + } else { + strcpy(reply, "ERR: clock cannot go backwards"); + } + } else if (memcmp(command, "clock", 5) == 0) { + uint32_t now = getRTCClock()->getCurrentTime(); + snprintf(reply, CLI_REPLY_SIZE, "Clock: %u", now); + } else if (memcmp(command, "time ", 5) == 0) { + uint32_t secs = _atoi(&command[5]); + uint32_t curr = getRTCClock()->getCurrentTime(); + if (secs > curr) { + getRTCClock()->setCurrentTime(secs); + snprintf(reply, CLI_REPLY_SIZE, "OK - clock set to %u", secs); + } else { + strcpy(reply, "(ERR: clock cannot go backwards)"); + } + } else if (memcmp(command, "neighbors", 9) == 0) { + _callbacks->formatNeighborsReply(reply); + } else if (memcmp(command, "neighbor.remove ", 16) == 0) { + const char* hex = &command[16]; + uint8_t pubkey[PUB_KEY_SIZE]; + int hex_len = strlen(hex); + if (hex_len > PUB_KEY_SIZE * 2) hex_len = PUB_KEY_SIZE * 2; + int pubkey_len = hex_len / 2; + if (mesh::Utils::fromHex(pubkey, pubkey_len, hex)) { + _callbacks->removeNeighbor(pubkey, pubkey_len); + strcpy(reply, "OK"); + } else { + strcpy(reply, "ERR: bad pubkey"); + } + } else if (memcmp(command, "tempradio ", 10) == 0) { + strcpy(tmp, &command[10]); + const char* parts[5]; + int num = mesh::Utils::parseTextParts(tmp, parts, 5); + float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; + float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; + uint8_t sf = num > 2 ? atoi(parts[2]) : 0; + uint8_t cr = num > 3 ? atoi(parts[3]) : 0; + int temp_timeout_mins = num > 4 ? atoi(parts[4]) : 0; + if (freq >= 300.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && + cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f && temp_timeout_mins > 0) { + _callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins); + snprintf(reply, CLI_REPLY_SIZE, "OK - temp params for %d mins", temp_timeout_mins); + } else { + strcpy(reply, "Error, invalid params"); + } + } else if (memcmp(command, "password ", 9) == 0) { + StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password)); + savePrefs(); + snprintf(reply, CLI_REPLY_SIZE, "password now: %s", _prefs->password); + } else if (memcmp(command, "clear stats", 11) == 0) { + _callbacks->clearStats(); + strcpy(reply, "(OK - stats reset)"); + /* + * GET commands + */ + } else if (memcmp(command, "get ", 4) == 0) { + const char* config = &command[4]; + if (memcmp(config, "af", 2) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %.0f%%", (double)_prefs->airtime_factor); + } else if (memcmp(config, "int.thresh", 10) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->interference_threshold); + } else if (memcmp(config, "agc.reset.interval", 18) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %u", ((uint32_t)_prefs->agc_reset_interval) * 4); + } else if (memcmp(config, "multi.acks", 10) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->multi_acks); + } else if (memcmp(config, "allow.read.only", 15) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->allow_read_only ? "on" : "off"); + } else if (memcmp(config, "flood.advert.interval", 21) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->flood_advert_interval); + } else if (memcmp(config, "advert.interval", 15) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %u", ((uint32_t)_prefs->advert_interval) * 2); + } else if (memcmp(config, "guest.password", 14) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->guest_password); + } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { + uint8_t prv_key[PRV_KEY_SIZE]; + int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE); + mesh::Utils::toHex(tmp, prv_key, len); + snprintf(reply, CLI_REPLY_SIZE, "> %s", tmp); + } else if (memcmp(config, "name", 4) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->node_name); + } else if (memcmp(config, "repeat", 6) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->disable_fwd ? "off" : "on"); + } else if (memcmp(config, "lat", 3) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %.6f", _prefs->node_lat); + } else if (memcmp(config, "lon", 3) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %.6f", _prefs->node_lon); + } else if (memcmp(config, "radio", 5) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %.3f,%.1f,%u,%u", + (double)_prefs->freq, (double)_prefs->bw, + (uint32_t)_prefs->sf, (uint32_t)_prefs->cr); + } else if (memcmp(config, "rxdelay", 7) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %.2f", (double)_prefs->rx_delay_base); + } else if (memcmp(config, "txdelay", 7) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %.2f", (double)_prefs->tx_delay_factor); + } else if (memcmp(config, "flood.max", 9) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->flood_max); + } else if (memcmp(config, "direct.txdelay", 14) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %.2f", (double)_prefs->direct_tx_delay_factor); + } else if (memcmp(config, "owner.info", 10) == 0) { + *reply++ = '>'; + *reply++ = ' '; + const char* sp = _prefs->owner_info; + while (*sp) { + *reply++ = (*sp == '\n') ? '|' : *sp; + sp++; + } + *reply = 0; + } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { + snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->tx_power_dbm); + } else if (memcmp(config, "freq", 4) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %.3f", (double)_prefs->freq); + } else if (memcmp(config, "public.key", 10) == 0) { + strcpy(reply, "> "); + mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); + } else if (memcmp(config, "role", 4) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %s", _callbacks->getRole()); + } else if (memcmp(config, "adc.multiplier", 14) == 0) { + float adc_mult = _board->getAdcMultiplier(); + if (adc_mult == 0.0f) { + strcpy(reply, "Error: unsupported by this board"); + } else { + snprintf(reply, CLI_REPLY_SIZE, "> %.3f", (double)adc_mult); + } + } else { + snprintf(reply, CLI_REPLY_SIZE, "??: %s", config); + } + /* + * SET commands + */ + } else if (memcmp(command, "set ", 4) == 0) { + const char* config = &command[4]; + if (memcmp(config, "af ", 3) == 0) { + _prefs->airtime_factor = atof(&config[3]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "int.thresh ", 11) == 0) { + _prefs->interference_threshold = atoi(&config[11]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { + _prefs->agc_reset_interval = atoi(&config[19]) / 4; + savePrefs(); + snprintf(reply, CLI_REPLY_SIZE, "OK - interval rounded to %u", ((uint32_t)_prefs->agc_reset_interval) * 4); + } else if (memcmp(config, "multi.acks ", 11) == 0) { + _prefs->multi_acks = atoi(&config[11]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "allow.read.only ", 16) == 0) { + _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "flood.advert.interval ", 22) == 0) { + int hours = _atoi(&config[22]); + if ((hours > 0 && hours < 3) || (hours > 168)) { + strcpy(reply, "Error: interval range is 3-168 hours"); + } else { + _prefs->flood_advert_interval = (uint8_t)hours; + _callbacks->updateFloodAdvertTimer(); + savePrefs(); + strcpy(reply, "OK"); + } + } else if (memcmp(config, "advert.interval ", 16) == 0) { + int mins = _atoi(&config[16]); + if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) { + snprintf(reply, CLI_REPLY_SIZE, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL); + } else { + _prefs->advert_interval = (uint8_t)(mins / 2); + _callbacks->updateAdvertTimer(); + savePrefs(); + strcpy(reply, "OK"); + } + } else if (memcmp(config, "guest.password ", 15) == 0) { + StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password)); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "prv.key ", 8) == 0) { + uint8_t prv_key[PRV_KEY_SIZE]; + bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]); + if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) { + mesh::LocalIdentity new_id; + new_id.readFrom(prv_key, PRV_KEY_SIZE); + _callbacks->saveIdentity(new_id); + strcpy(reply, "OK, reboot to apply! New pubkey: "); + mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE); + } else { + strcpy(reply, "Error, bad key"); + } + } else if (memcmp(config, "name ", 5) == 0) { + if (isValidName(&config[5])) { + StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, bad chars"); + } + } else if (memcmp(config, "repeat ", 7) == 0) { + _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; + savePrefs(); + strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); + } else if (memcmp(config, "radio ", 6) == 0) { + strcpy(tmp, &config[6]); + const char* parts[4]; + int num = mesh::Utils::parseTextParts(tmp, parts, 4); + float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; + float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; + uint8_t sf = num > 2 ? atoi(parts[2]) : 0; + uint8_t cr = num > 3 ? atoi(parts[3]) : 0; + if (freq >= 300.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && + cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { + _prefs->sf = sf; + _prefs->cr = cr; + _prefs->freq = freq; + _prefs->bw = bw; + _callbacks->savePrefs(); + strcpy(reply, "OK - reboot to apply"); + } else { + strcpy(reply, "Error, invalid radio params"); + } + } else if (memcmp(config, "lat ", 4) == 0) { + _prefs->node_lat = atof(&config[4]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "lon ", 4) == 0) { + _prefs->node_lon = atof(&config[4]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "rxdelay ", 8) == 0) { + float db = atof(&config[8]); + if (db >= 0) { + _prefs->rx_delay_base = db; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "txdelay ", 8) == 0) { + float f = atof(&config[8]); + if (f >= 0) { + _prefs->tx_delay_factor = f; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "flood.max ", 10) == 0) { + uint8_t m = atoi(&config[10]); + if (m <= 64) { + _prefs->flood_max = m; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, max 64"); + } + } else if (memcmp(config, "direct.txdelay ", 15) == 0) { + float f = atof(&config[15]); + if (f >= 0) { + _prefs->direct_tx_delay_factor = f; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "owner.info ", 11) == 0) { + config += 11; + char* dp = _prefs->owner_info; + while (*config && dp - _prefs->owner_info < (int)sizeof(_prefs->owner_info) - 1) { + *dp++ = (*config == '|') ? '\n' : *config; + config++; + } + *dp = 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "tx ", 3) == 0) { + _prefs->tx_power_dbm = atoi(&config[3]); + savePrefs(); + _callbacks->setTxPower(_prefs->tx_power_dbm); + strcpy(reply, "OK"); + } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { + _prefs->freq = atof(&config[5]); + savePrefs(); + strcpy(reply, "OK - reboot to apply"); + } else if (memcmp(config, "adc.multiplier ", 15) == 0) { + _prefs->adc_multiplier = atof(&config[15]); + if (_board->setAdcMultiplier(_prefs->adc_multiplier)) { + savePrefs(); + if (_prefs->adc_multiplier == 0.0f) { + strcpy(reply, "OK - using default board multiplier"); + } else { + snprintf(reply, CLI_REPLY_SIZE, "OK - multiplier set to %.3f", (double)_prefs->adc_multiplier); + } + } else { + _prefs->adc_multiplier = 0.0f; + strcpy(reply, "Error: unsupported by this board"); + } + } else if (memcmp(config, "rxboost ", 8) == 0) { + _prefs->rx_boost = atoi(&config[8]) ? 1 : 0; + savePrefs(); + snprintf(reply, CLI_REPLY_SIZE, "OK - rxboost=%d (reboot to apply)", _prefs->rx_boost); + } else if (memcmp(config, "rxboost", 7) == 0 && (config[7] == 0 || config[7] == ' ')) { + snprintf(reply, CLI_REPLY_SIZE, "> %d", _prefs->rx_boost); + } else { + snprintf(reply, CLI_REPLY_SIZE, "unknown config: %s", config); + } + } else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) { + bool s = _callbacks->formatFileSystem(); + snprintf(reply, CLI_REPLY_SIZE, "File system erase: %s", s ? "OK" : "Err"); + } else if (memcmp(command, "ver", 3) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate()); + } else if (memcmp(command, "board", 5) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "%s", _board->getManufacturerName()); + } else if (memcmp(command, "sensor get ", 11) == 0) { + const char* key = command + 11; + const char* val = _callbacks->getSensorSettingByKey(key); + if (val != nullptr) { + snprintf(reply, CLI_REPLY_SIZE, "> %s", val); + } else { + strcpy(reply, "null"); + } + } else if (memcmp(command, "sensor set ", 11) == 0) { + strcpy(tmp, &command[11]); + const char* parts[2]; + int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' '); + const char* key = (num > 0) ? parts[0] : ""; + const char* value = (num > 1) ? parts[1] : "null"; + if (_callbacks->setSensorSettingValue(key, value)) { + strcpy(reply, "ok"); + } else { + strcpy(reply, "can't find custom var"); + } + } else if (memcmp(command, "sensor list", 11) == 0) { + char* dp = reply; + int start = 0; + int end = _callbacks->getNumSensorSettings(); + if (strlen(command) > 11) { + start = _atoi(command + 12); + } + if (start >= end) { + strcpy(reply, "no custom var"); + } else { + snprintf(dp, CLI_REPLY_SIZE - (dp - reply), "%d vars\n", end); + dp = strchr(dp, 0); + int i; + for (i = start; i < end && (dp - reply < 134); i++) { + snprintf(dp, CLI_REPLY_SIZE - (dp - reply), "%s=%s\n", + _callbacks->getSensorSettingName(i), + _callbacks->getSensorSettingValue(i)); + dp = strchr(dp, 0); + } + if (i < end) { + snprintf(dp, CLI_REPLY_SIZE - (dp - reply), "... next:%d", i); + } else { + *(dp - 1) = 0; // remove last CR + } + } + } else if (memcmp(command, "gps on", 6) == 0) { + if (_callbacks->setGpsEnabled(true)) { + _prefs->gps_enabled = 1; + savePrefs(); + strcpy(reply, "ok"); + } else { + strcpy(reply, "gps toggle not found"); + } + } else if (memcmp(command, "gps off", 7) == 0) { + if (_callbacks->setGpsEnabled(false)) { + _prefs->gps_enabled = 0; + savePrefs(); + strcpy(reply, "ok"); + } else { + strcpy(reply, "gps toggle not found"); + } + } else if (memcmp(command, "gps setloc", 10) == 0) { + _prefs->node_lat = _callbacks->getNodeLat(); + _prefs->node_lon = _callbacks->getNodeLon(); + savePrefs(); + strcpy(reply, "ok"); + } else if (memcmp(command, "gps advert", 10) == 0) { + if (strlen(command) == 10) { + switch (_prefs->advert_loc_policy) { + case ADVERT_LOC_NONE: strcpy(reply, "> none"); break; + case ADVERT_LOC_PREFS: strcpy(reply, "> prefs"); break; + case ADVERT_LOC_SHARE: strcpy(reply, "> share"); break; + default: strcpy(reply, "error"); + } + } else if (memcmp(command + 11, "none", 4) == 0) { + _prefs->advert_loc_policy = ADVERT_LOC_NONE; + savePrefs(); + strcpy(reply, "ok"); + } else if (memcmp(command + 11, "share", 5) == 0) { + _prefs->advert_loc_policy = ADVERT_LOC_SHARE; + savePrefs(); + strcpy(reply, "ok"); + } else if (memcmp(command + 11, "prefs", 4) == 0) { + _prefs->advert_loc_policy = ADVERT_LOC_PREFS; + savePrefs(); + strcpy(reply, "ok"); + } else { + strcpy(reply, "error"); + } + } else if (memcmp(command, "gps", 3) == 0) { + if (_callbacks->isGpsEnabled()) { + strcpy(reply, "on"); + } else { + strcpy(reply, "off"); + } + } else if (memcmp(command, "powersaving on", 14) == 0) { + _prefs->powersaving_enabled = 1; + savePrefs(); + strcpy(reply, "ok"); + } else if (memcmp(command, "powersaving off", 15) == 0) { + _prefs->powersaving_enabled = 0; + savePrefs(); + strcpy(reply, "ok"); + } else if (memcmp(command, "powersaving", 11) == 0) { + strcpy(reply, _prefs->powersaving_enabled ? "on" : "off"); + } else if (memcmp(command, "log start", 9) == 0) { + _callbacks->setLoggingOn(true); + strcpy(reply, " logging on"); + } else if (memcmp(command, "log stop", 8) == 0) { + _callbacks->setLoggingOn(false); + strcpy(reply, " logging off"); + } else if (memcmp(command, "log erase", 9) == 0) { + _callbacks->eraseLogFile(); + strcpy(reply, " log erased"); + } else if (sender_timestamp == 0 && memcmp(command, "log", 3) == 0) { + _callbacks->dumpLogFile(); + strcpy(reply, " EOF"); + } else if (sender_timestamp == 0 && memcmp(command, "stats-packets", 13) == 0 && + (command[13] == 0 || command[13] == ' ')) { + _callbacks->formatPacketStatsReply(reply); + } else if (sender_timestamp == 0 && memcmp(command, "stats-radio", 11) == 0 && + (command[11] == 0 || command[11] == ' ')) { + _callbacks->formatRadioStatsReply(reply); + } else if (sender_timestamp == 0 && memcmp(command, "stats-core", 10) == 0 && + (command[10] == 0 || command[10] == ' ')) { + _callbacks->formatStatsReply(reply); + } else { + strcpy(reply, "Unknown command"); + } +} diff --git a/zephcore/helpers/CommonCLI.h b/zephcore/helpers/CommonCLI.h new file mode 100644 index 0000000..85d600e --- /dev/null +++ b/zephcore/helpers/CommonCLI.h @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * CommonCLI - Common CLI command handlers for repeaters + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "NodePrefs.h" + +/* CLI reply buffer size — callers must provide at least this many bytes */ +#define CLI_REPLY_SIZE 256 + +/* Deferred reboot types */ +#define REBOOT_NONE 0 +#define REBOOT_NORMAL 1 +#define REBOOT_DFU 2 +#define REBOOT_OTA 3 + +class CommonCLICallbacks { +public: + virtual void savePrefs() = 0; + virtual const char* getFirmwareVer() = 0; + virtual const char* getBuildDate() = 0; + virtual const char* getRole() = 0; + virtual bool formatFileSystem() = 0; + virtual void sendSelfAdvertisement(int delay_millis, bool flood) = 0; + virtual void updateAdvertTimer() = 0; + virtual void updateFloodAdvertTimer() = 0; + virtual void setLoggingOn(bool enable) = 0; + virtual void eraseLogFile() = 0; + virtual void dumpLogFile() = 0; + virtual void setTxPower(uint8_t power_dbm) = 0; + virtual void formatNeighborsReply(char* reply) = 0; + virtual void removeNeighbor(const uint8_t* pubkey, int key_len) { + // no-op by default + } + virtual void formatStatsReply(char* reply) = 0; + virtual void formatRadioStatsReply(char* reply) = 0; + virtual void formatPacketStatsReply(char* reply) = 0; + virtual mesh::LocalIdentity& getSelfId() = 0; + virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0; + virtual void clearStats() = 0; + virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0; + + // Sensor manager interface (for GPS) + virtual double getNodeLat() const { return 0.0; } + virtual double getNodeLon() const { return 0.0; } + virtual bool setGpsEnabled(bool enabled) { return false; } + virtual bool isGpsEnabled() const { return false; } + virtual int getNumSensorSettings() const { return 0; } + virtual const char* getSensorSettingName(int idx) const { return nullptr; } + virtual const char* getSensorSettingValue(int idx) const { return nullptr; } + virtual const char* getSensorSettingByKey(const char* key) const { return nullptr; } + virtual bool setSensorSettingValue(const char* key, const char* value) { return false; } +}; + +class CommonCLI { + /* Members ordered to match constructor initialization order */ + mesh::MainBoard* _board; + mesh::RTCClock* _rtc; + ClientACL* _acl; + NodePrefs* _prefs; + CommonCLICallbacks* _callbacks; + char tmp[PRV_KEY_SIZE * 2 + 4]; + + /* Deferred reboot - lets LoRa reply be sent before rebooting */ + struct k_work_delayable _reboot_work; + uint8_t _pending_reboot; + static void rebootWorkHandler(struct k_work *work); + + mesh::RTCClock* getRTCClock() { return _rtc; } + void savePrefs(); + void scheduleReboot(uint8_t type); + +public: + CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, ClientACL& acl, + NodePrefs* prefs, CommonCLICallbacks* callbacks) + : _board(&board), _rtc(&rtc), _acl(&acl), _prefs(prefs), _callbacks(callbacks), + _pending_reboot(REBOOT_NONE) + { + k_work_init_delayable(&_reboot_work, rebootWorkHandler); + } + + void loadPrefs(const char* path); + void savePrefs(const char* path); + void handleCommand(uint32_t sender_timestamp, const char* command, char* reply); + uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data); +}; diff --git a/zephcore/helpers/ContactInfo.h b/zephcore/helpers/ContactInfo.h new file mode 100644 index 0000000..1a86a6c --- /dev/null +++ b/zephcore/helpers/ContactInfo.h @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ContactInfo - contact/peer info for mesh + */ + +#pragma once + +#include +#include + +struct ContactInfo { + mesh::Identity id; + char name[32]; + uint8_t type; + uint8_t flags; + int8_t out_path_len; + mutable bool shared_secret_valid; + uint8_t out_path[MAX_PATH_SIZE]; + uint32_t last_advert_timestamp; + uint32_t lastmod; + int32_t gps_lat, gps_lon; + uint32_t sync_since; + + const uint8_t *getSharedSecret(const mesh::LocalIdentity &self_id) const { + if (!shared_secret_valid) { + self_id.calcSharedSecret(shared_secret, id.pub_key); + shared_secret_valid = true; + } + return shared_secret; + } + +private: + mutable uint8_t shared_secret[PUB_KEY_SIZE]; +}; diff --git a/zephcore/helpers/IdentityStore.h b/zephcore/helpers/IdentityStore.h new file mode 100644 index 0000000..16930c2 --- /dev/null +++ b/zephcore/helpers/IdentityStore.h @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * IdentityStore.h - Compatibility header for ZephCore constants + * + * This header provides common identity and crypto constants used + * by repeater helpers. It simply re-exports from mesh/MeshCore.h. + */ + +#pragma once + +#include +#include diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h new file mode 100644 index 0000000..e9b660e --- /dev/null +++ b/zephcore/helpers/NodePrefs.h @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * NodePrefs - persisted node configuration (unified for all roles) + * + * Matches Arduino MeshCore's single NodePrefs struct. + * Both companion and repeater use this same type. Role-specific + * fields are simply unused by the other role. + * + * Storage format is field-by-field (not raw memcpy), so struct + * layout doesn't affect on-disk compatibility. + */ + +#pragma once + +#include +#include + +#define TELEM_MODE_DENY 0 +#define TELEM_MODE_ALLOW_FLAGS 1 +#define TELEM_MODE_ALLOW_ALL 2 + +#define ADVERT_LOC_NONE 0 +#define ADVERT_LOC_SHARE 1 +#define ADVERT_LOC_PREFS 2 + +struct NodePrefs { + /* ---- Common fields (both roles) ---- */ + float airtime_factor; + char node_name[32]; + double node_lat, node_lon; + char password[16]; + float freq; + uint8_t tx_power_dbm; + uint8_t disable_fwd; // repeater: disable forwarding + uint8_t advert_interval; // minutes / 2 + uint8_t flood_advert_interval; // hours + float rx_delay_base; + float tx_delay_factor; + char guest_password[16]; + float direct_tx_delay_factor; + uint32_t guard; + uint8_t sf; + uint8_t cr; + uint8_t allow_read_only; + uint8_t multi_acks; + float bw; + uint8_t flood_max; + uint8_t interference_threshold; + uint8_t agc_reset_interval; // secs / 4 + // Power saving + uint8_t powersaving_enabled; + // GPS settings + uint8_t gps_enabled; + uint32_t gps_interval; // in seconds + uint8_t advert_loc_policy; + uint32_t discovery_mod_timestamp; + float adc_multiplier; + char owner_info[120]; + uint8_t rx_boost; // 1 = boosted RX gain (+3dB, +2mA), 0 = power save + + /* ---- Companion-only fields (Zephyr additions, not in Arduino) ---- */ + uint8_t manual_add_contacts; + uint8_t telemetry_mode_base; + uint8_t telemetry_mode_loc; + uint8_t telemetry_mode_env; + uint32_t ble_pin; + uint8_t buzzer_quiet; + uint8_t autoadd_config; + uint8_t client_repeat; // 1 = offgrid mode (forward packets), 0 = companion only +}; + +/* Default prefs — MUST match LoRaConfig.h defaults for radio interop. + * Used by both roles; each role's data store calls this on first boot. */ +static inline void initNodePrefs(NodePrefs* prefs) { + memset(prefs, 0, sizeof(NodePrefs)); + prefs->airtime_factor = 10.0f; /* 10% duty cycle (EU 868 default) */ + /* node_name left empty — each role sets its own default name */ + prefs->node_lat = 0.0; + prefs->node_lon = 0.0; +#ifdef CONFIG_ZEPHCORE_ADMIN_PASSWORD + strncpy(prefs->password, CONFIG_ZEPHCORE_ADMIN_PASSWORD, sizeof(prefs->password) - 1); +#else + strcpy(prefs->password, "password"); +#endif +#ifdef CONFIG_ZEPHCORE_GUEST_PASSWORD + strncpy(prefs->guest_password, CONFIG_ZEPHCORE_GUEST_PASSWORD, sizeof(prefs->guest_password) - 1); +#endif + /* Radio params - MUST match LoRaConfig.h for interop with companion nodes */ + prefs->freq = 869.618f; // LoRaConfig::FREQ_HZ / 1000000.0 + prefs->bw = 62.5f; // LoRaConfig::BANDWIDTH + prefs->sf = 8; // LoRaConfig::SPREADING_FACTOR + prefs->cr = 8; // CR 4/8 (MeshCore uses 5-8 for CR 4/5 through 4/8) + prefs->tx_power_dbm = 22; // LoRaConfig::TX_POWER_DBM + prefs->disable_fwd = 0; + prefs->advert_interval = 60; // 2 minutes (value / 2) + prefs->flood_advert_interval = 12; // 12 hours + prefs->rx_delay_base = 0.0f; + prefs->tx_delay_factor = 0.5f; + prefs->direct_tx_delay_factor = 0.3f; + prefs->allow_read_only = 0; + prefs->multi_acks = 0; + prefs->flood_max = 64; // max hops for flood packets (0 = blocking all!) + prefs->interference_threshold = 0; + prefs->agc_reset_interval = 0; + prefs->powersaving_enabled = 0; + prefs->gps_enabled = 0; + prefs->gps_interval = 300; // 5 minutes + prefs->advert_loc_policy = ADVERT_LOC_NONE; + prefs->adc_multiplier = 0.0f; + prefs->rx_boost = 1; // Default to boosted RX for better sensitivity +} diff --git a/zephcore/helpers/RateLimiter.h b/zephcore/helpers/RateLimiter.h new file mode 100644 index 0000000..17e1b73 --- /dev/null +++ b/zephcore/helpers/RateLimiter.h @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * RateLimiter - simple rate limiting for repeaters + */ + +#pragma once + +#include + +class RateLimiter { + /* Members ordered to match constructor initialization order */ + uint16_t _maximum; + uint32_t _secs; + uint32_t _start_timestamp; + uint16_t _count; + +public: + RateLimiter(uint16_t maximum, uint32_t secs) + : _maximum(maximum), _secs(secs), _start_timestamp(0), _count(0) {} + + bool allow(uint32_t now) { + if (now < _start_timestamp + _secs) { + _count++; + if (_count > _maximum) return false; // deny + } else { + // time window now expired + _start_timestamp = now; + _count = 1; + } + return true; + } + + void reset() { + _start_timestamp = 0; + _count = 0; + } +}; diff --git a/zephcore/helpers/RegionMap.cpp b/zephcore/helpers/RegionMap.cpp new file mode 100644 index 0000000..79758a1 --- /dev/null +++ b/zephcore/helpers/RegionMap.cpp @@ -0,0 +1,310 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * RegionMap - Region-based flood filtering for repeaters + */ + +#include "RegionMap.h" +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(zephcore_regions, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); + +static const char* skip_hash(const char* name) { + return *name == '#' ? name + 1 : name; +} + +RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) { + next_id = 1; + num_regions = 0; + home_id = 0; + wildcard.id = 0; + wildcard.parent = 0; + wildcard.flags = 0; // default behaviour, allow flood and direct + strcpy(wildcard.name, "*"); +} + +bool RegionMap::is_name_char(uint8_t c) { + // accept all alpha-num or accented characters, but exclude most punctuation chars + return c == '-' || c == '$' || c == '#' || (c >= '0' && c <= '9') || c >= 'A'; +} + +bool RegionMap::load(const char* path) { + const char* filepath = path ? path : "/lfs/repeater/regions2"; + + struct fs_file_t file; + fs_file_t_init(&file); + + if (fs_open(&file, filepath, FS_O_READ) < 0) { + LOG_DBG("No regions file at %s", filepath); + return false; + } + + uint8_t pad[128]; + num_regions = 0; + next_id = 1; + home_id = 0; + + bool success = fs_read(&file, pad, 5) == 5; // reserved header + success = success && fs_read(&file, &home_id, sizeof(home_id)) == sizeof(home_id); + success = success && fs_read(&file, &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags); + success = success && fs_read(&file, &next_id, sizeof(next_id)) == sizeof(next_id); + + if (success) { + while (num_regions < MAX_REGION_ENTRIES) { + auto r = ®ions[num_regions]; + + success = fs_read(&file, &r->id, sizeof(r->id)) == sizeof(r->id); + success = success && fs_read(&file, &r->parent, sizeof(r->parent)) == sizeof(r->parent); + success = success && fs_read(&file, r->name, sizeof(r->name)) == sizeof(r->name); + success = success && fs_read(&file, &r->flags, sizeof(r->flags)) == sizeof(r->flags); + success = success && fs_read(&file, pad, sizeof(pad)) == sizeof(pad); + + if (!success) break; // EOF + + if (r->id >= next_id) { // make sure next_id is valid + next_id = r->id + 1; + } + num_regions++; + } + } + fs_close(&file); + LOG_INF("Loaded %d regions from %s", num_regions, filepath); + return true; +} + +bool RegionMap::save(const char* path) { + const char* filepath = path ? path : "/lfs/repeater/regions2"; + + // Remove old file first + fs_unlink(filepath); + + struct fs_file_t file; + fs_file_t_init(&file); + + if (fs_open(&file, filepath, FS_O_CREATE | FS_O_WRITE) < 0) { + LOG_ERR("Failed to open %s for write", filepath); + return false; + } + + uint8_t pad[128]; + memset(pad, 0, sizeof(pad)); + + bool success = fs_write(&file, pad, 5) == 5; // reserved header + success = success && fs_write(&file, &home_id, sizeof(home_id)) == sizeof(home_id); + success = success && fs_write(&file, &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags); + success = success && fs_write(&file, &next_id, sizeof(next_id)) == sizeof(next_id); + + if (success) { + for (int i = 0; i < num_regions; i++) { + auto r = ®ions[i]; + + success = fs_write(&file, &r->id, sizeof(r->id)) == sizeof(r->id); + success = success && fs_write(&file, &r->parent, sizeof(r->parent)) == sizeof(r->parent); + success = success && fs_write(&file, r->name, sizeof(r->name)) == sizeof(r->name); + success = success && fs_write(&file, &r->flags, sizeof(r->flags)) == sizeof(r->flags); + success = success && fs_write(&file, pad, sizeof(pad)) == sizeof(pad); + + if (!success) break; // write failed + } + } + fs_close(&file); + LOG_INF("Saved %d regions to %s", num_regions, filepath); + return true; +} + +RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t id) { + const char* sp = name; // check for illegal name chars + while (*sp) { + if (!is_name_char(*sp)) return nullptr; // error + sp++; + } + + auto region = findByName(name); + if (region) { + if (region->id == parent_id) return nullptr; // ERROR: invalid parent! + region->parent = parent_id; // re-parent / move this region in the hierarchy + } else { + if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return nullptr; // full! + + region = ®ions[num_regions++]; // alloc new RegionEntry + region->flags = REGION_DENY_FLOOD; // DENY by default + region->id = id == 0 ? next_id++ : id; + StrHelper::strncpy(region->name, name, sizeof(region->name)); + region->parent = parent_id; + } + return region; +} + +RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) { + for (int i = 0; i < num_regions; i++) { + auto region = ®ions[i]; + if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param) + TransportKey keys[4]; + int num; + if (region->name[0] == '$') { // private region + num = _store->loadKeysFor(region->id, keys, 4); + } else if (region->name[0] == '#') { // auto hashtag region + _store->getAutoKeyFor(region->id, region->name, keys[0]); + num = 1; + } else { // new: implicit auto hashtag region + char tmp[sizeof(region->name)]; + tmp[0] = '#'; + strcpy(&tmp[1], region->name); + _store->getAutoKeyFor(region->id, tmp, keys[0]); + num = 1; + } + for (int j = 0; j < num; j++) { + uint16_t code = keys[j].calcTransportCode(packet); + if (packet->transport_codes[0] == code) { // a match!! + return region; + } + } + } + } + return nullptr; // no matches +} + +RegionEntry* RegionMap::findByName(const char* name) { + if (strcmp(name, "*") == 0) return &wildcard; + + if (*name == '#') { name++; } // ignore the '#' when matching by name + for (int i = 0; i < num_regions; i++) { + auto region = ®ions[i]; + if (strcmp(name, skip_hash(region->name)) == 0) return region; + } + return nullptr; // not found +} + +RegionEntry* RegionMap::findByNamePrefix(const char* prefix) { + if (strcmp(prefix, "*") == 0) return &wildcard; + + if (*prefix == '#') { prefix++; } // ignore the '#' when matching by name + RegionEntry* partial = nullptr; + for (int i = 0; i < num_regions; i++) { + auto region = ®ions[i]; + if (strcmp(prefix, skip_hash(region->name)) == 0) return region; // complete match + if (memcmp(prefix, skip_hash(region->name), strlen(prefix)) == 0) { + partial = region; + } + } + return partial; +} + +RegionEntry* RegionMap::findById(uint16_t id) { + if (id == 0) return &wildcard; // special root Region + + for (int i = 0; i < num_regions; i++) { + auto region = ®ions[i]; + if (region->id == id) return region; + } + return nullptr; // not found +} + +RegionEntry* RegionMap::getHomeRegion() { + return findById(home_id); +} + +void RegionMap::setHomeRegion(const RegionEntry* home) { + home_id = home ? home->id : 0; +} + +bool RegionMap::removeRegion(const RegionEntry& region) { + if (region.id == 0) return false; // cannot remove wildcard + + // first check region has no child regions + for (int i = 0; i < num_regions; i++) { + if (regions[i].parent == region.id) return false; // must remove children first + } + + int i = 0; + while (i < num_regions) { + if (region.id == regions[i].id) break; + i++; + } + if (i >= num_regions) return false; // not found + + num_regions--; // remove from regions array + while (i < num_regions) { + regions[i] = regions[i + 1]; + i++; + } + return true; +} + +bool RegionMap::clear() { + num_regions = 0; + return true; +} + +void RegionMap::printChildRegions(int indent, const RegionEntry* parent, char* buf, int& pos, int max_len) const { + // Print indentation + for (int i = 0; i < indent && pos < max_len - 1; i++) { + buf[pos++] = ' '; + } + + // Print region info + int written; + if (parent->flags & REGION_DENY_FLOOD) { + written = snprintf(&buf[pos], max_len - pos, "%s%s\n", + skip_hash(parent->name), + parent->id == home_id ? "^" : ""); + } else { + written = snprintf(&buf[pos], max_len - pos, "%s%s F\n", + skip_hash(parent->name), + parent->id == home_id ? "^" : ""); + } + if (written > 0 && pos + written < max_len) { + pos += written; + } + + // Print children recursively + for (int i = 0; i < num_regions; i++) { + auto r = ®ions[i]; + if (r->parent == parent->id) { + printChildRegions(indent + 1, r, buf, pos, max_len); + } + } +} + +size_t RegionMap::exportTo(char* dest, size_t max_len) const { + if (!dest || max_len == 0) return 0; + + int pos = 0; + printChildRegions(0, &wildcard, dest, pos, (int)max_len); + return (size_t)pos; +} + +int RegionMap::exportNamesTo(char* dest, int max_len, uint8_t mask, bool invert) { + char* dp = dest; + + // Check wildcard region + bool wildcard_matches = invert ? (wildcard.flags & mask) : !(wildcard.flags & mask); + if (wildcard_matches) { + *dp++ = '*'; + *dp++ = ','; + } + + for (int i = 0; i < num_regions; i++) { + auto region = ®ions[i]; + + // Check if region matches the filter criteria + bool region_matches = invert ? (region->flags & mask) : !(region->flags & mask); + + if (region_matches) { + int len = strlen(skip_hash(region->name)); + if ((dp - dest) + len + 2 < max_len) { // only append if name will fit + memcpy(dp, skip_hash(region->name), len); + dp += len; + *dp++ = ','; + } + } + } + + if (dp > dest) { dp--; } // don't include trailing comma + + *dp = 0; // set null terminator + return dp - dest; +} diff --git a/zephcore/helpers/RegionMap.h b/zephcore/helpers/RegionMap.h new file mode 100644 index 0000000..57db3d8 --- /dev/null +++ b/zephcore/helpers/RegionMap.h @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * RegionMap - Region-based flood filtering for repeaters + */ + +#pragma once + +#include +#include "TransportKeyStore.h" +#include +#include + +#ifndef MAX_REGION_ENTRIES + #ifdef CONFIG_ZEPHCORE_MAX_REGION_ENTRIES + #define MAX_REGION_ENTRIES CONFIG_ZEPHCORE_MAX_REGION_ENTRIES + #else + #define MAX_REGION_ENTRIES 32 + #endif +#endif + +#define REGION_DENY_FLOOD 0x01 +#define REGION_DENY_DIRECT 0x02 // reserved for future + +struct RegionEntry { + uint16_t id; + uint16_t parent; + uint8_t flags; + char name[31]; +}; + +class RegionMap { + TransportKeyStore* _store; + uint16_t next_id; + uint16_t home_id; + uint16_t num_regions; + RegionEntry regions[MAX_REGION_ENTRIES]; + RegionEntry wildcard; + + void printChildRegions(int indent, const RegionEntry* parent, char* buf, int& pos, int max_len) const; + +public: + RegionMap(TransportKeyStore& store); + + static bool is_name_char(uint8_t c); + + bool load(const char* path = nullptr); + bool save(const char* path = nullptr); + + RegionEntry* putRegion(const char* name, uint16_t parent_id, uint16_t id = 0); + RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask); + RegionEntry& getWildcard() { return wildcard; } + RegionEntry* findByName(const char* name); + RegionEntry* findByNamePrefix(const char* prefix); + RegionEntry* findById(uint16_t id); + RegionEntry* getHomeRegion(); // NOTE: can be NULL + void setHomeRegion(const RegionEntry* home); + bool removeRegion(const RegionEntry& region); + bool clear(); + void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; } + int getCount() const { return num_regions; } + const RegionEntry* getByIdx(int i) const { return ®ions[i]; } + const RegionEntry* getRoot() const { return &wildcard; } + int exportNamesTo(char* dest, int max_len, uint8_t mask, bool invert = false); + + size_t exportTo(char* dest, size_t max_len) const; +}; diff --git a/zephcore/helpers/StatsFormatHelper.h b/zephcore/helpers/StatsFormatHelper.h new file mode 100644 index 0000000..178e19b --- /dev/null +++ b/zephcore/helpers/StatsFormatHelper.h @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * StatsFormatHelper - Format statistics JSON for CLI/protocol + */ + +#pragma once + +#include +#include + +class StatsFormatHelper { +public: + static void formatCoreStats(char* reply, + mesh::MainBoard& board, + mesh::MillisecondClock& ms, + uint16_t err_flags, + mesh::PacketManager* mgr) { + sprintf(reply, + "{\"battery_mv\":%u,\"uptime_secs\":%u,\"errors\":%u,\"queue_len\":%u}", + board.getBattMilliVolts(), + (unsigned)(ms.getMillis() / 1000), + err_flags, + mgr->getOutboundCount(0xFFFFFFFF) + ); + } + + template + static void formatRadioStats(char* reply, + mesh::Radio* radio, + RadioDriverType& driver, + uint32_t total_air_time_ms, + uint32_t total_rx_air_time_ms) { + sprintf(reply, + "{\"noise_floor\":%d,\"last_rssi\":%d,\"last_snr\":%.2f,\"tx_air_secs\":%u,\"rx_air_secs\":%u}", + (int16_t)radio->getNoiseFloor(), + (int16_t)driver.getLastRSSI(), + (double)driver.getLastSNR(), + total_air_time_ms / 1000, + total_rx_air_time_ms / 1000 + ); + } + + template + static void formatPacketStats(char* reply, + RadioDriverType& driver, + uint32_t n_sent_flood, + uint32_t n_sent_direct, + uint32_t n_recv_flood, + uint32_t n_recv_direct) { + sprintf(reply, + "{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u,\"recv_errors\":%u}", + driver.getPacketsRecv(), + driver.getPacketsSent(), + n_sent_flood, + n_sent_direct, + n_recv_flood, + n_recv_direct, + driver.getPacketsRecvErrors() + ); + } +}; diff --git a/zephcore/helpers/TransportKeyStore.cpp b/zephcore/helpers/TransportKeyStore.cpp new file mode 100644 index 0000000..76e4b5e --- /dev/null +++ b/zephcore/helpers/TransportKeyStore.cpp @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * TransportKeyStore - Transport key cache for region filtering + */ + +#include "TransportKeyStore.h" +#include +#include +#include + +uint16_t TransportKey::calcTransportCode(const mesh::Packet* packet) const { + /* HMAC-SHA256 using mbedTLS */ + uint8_t hmac[32]; + uint8_t type = packet->getPayloadType(); + + /* Build message: type + payload */ + uint8_t msg[MAX_PACKET_PAYLOAD + 1]; + msg[0] = type; + memcpy(&msg[1], packet->payload, packet->payload_len); + size_t msg_len = 1 + packet->payload_len; + + /* Calculate HMAC-SHA256 */ + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1); + mbedtls_md_hmac_starts(&ctx, key, sizeof(key)); + mbedtls_md_hmac_update(&ctx, msg, msg_len); + mbedtls_md_hmac_finish(&ctx, hmac); + mbedtls_md_free(&ctx); + + /* Extract first 2 bytes as transport code */ + uint16_t code = (hmac[0] << 8) | hmac[1]; + if (code == 0) { // reserve codes 0000 and FFFF + code++; + } else if (code == 0xFFFF) { + code--; + } + return code; +} + +bool TransportKey::isNull() const { + for (size_t i = 0; i < sizeof(key); i++) { + if (key[i]) return false; + } + return true; // key is all zeroes +} + +void TransportKeyStore::putCache(uint16_t id, const TransportKey& key) { + if (num_cache < MAX_TKS_ENTRIES) { + cache_ids[num_cache] = id; + cache_keys[num_cache] = key; + num_cache++; + } else { + // TODO: evict oldest cache entry + } +} + +void TransportKeyStore::getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest) { + // first, check cache + for (int i = 0; i < num_cache; i++) { + if (cache_ids[i] == id) { // cache hit! + dest = cache_keys[i]; + return; + } + } + + // calc key for publicly-known hashtag region name (SHA256 hash, first 16 bytes) + uint8_t hash[32]; + mbedtls_sha256((const unsigned char*)name, strlen(name), hash, 0); + memcpy(dest.key, hash, sizeof(dest.key)); + + putCache(id, dest); +} + +int TransportKeyStore::loadKeysFor(uint16_t id, TransportKey keys[], int max_num) { + int n = 0; + // first, check cache + for (int i = 0; i < num_cache && n < max_num; i++) { + if (cache_ids[i] == id) { + keys[n++] = cache_keys[i]; + } + } + if (n > 0) return n; // cache hit! + + // TODO: retrieve from hardware keystore + + // store in cache (if room) + for (int i = 0; i < n; i++) { + putCache(id, keys[i]); + } + return n; +} + +bool TransportKeyStore::saveKeysFor(uint16_t id, const TransportKey keys[], int num) { + invalidateCache(); + // TODO: update hardware keystore + return false; // failed +} + +bool TransportKeyStore::removeKeys(uint16_t id) { + invalidateCache(); + // TODO: remove from hardware keystore + return false; // failed +} + +bool TransportKeyStore::clear() { + invalidateCache(); + // TODO: clear hardware keystore + return false; // failed +} diff --git a/zephcore/helpers/TransportKeyStore.h b/zephcore/helpers/TransportKeyStore.h new file mode 100644 index 0000000..d7c4275 --- /dev/null +++ b/zephcore/helpers/TransportKeyStore.h @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * TransportKeyStore - Transport key cache for region filtering + */ + +#pragma once + +#include +#include +#include +#include + +struct TransportKey { + uint8_t key[16]; + + uint16_t calcTransportCode(const mesh::Packet* packet) const; + bool isNull() const; +}; + +#define MAX_TKS_ENTRIES 16 + +class TransportKeyStore { + uint16_t cache_ids[MAX_TKS_ENTRIES]; + TransportKey cache_keys[MAX_TKS_ENTRIES]; + int num_cache; + + void putCache(uint16_t id, const TransportKey& key); + void invalidateCache() { num_cache = 0; } + +public: + TransportKeyStore() : num_cache(0) {} + + void getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest); + int loadKeysFor(uint16_t id, TransportKey keys[], int max_num); + bool saveKeysFor(uint16_t id, const TransportKey keys[], int num); + bool removeKeys(uint16_t id); + bool clear(); +}; diff --git a/zephcore/helpers/TxtDataHelpers.h b/zephcore/helpers/TxtDataHelpers.h new file mode 100644 index 0000000..0b917f8 --- /dev/null +++ b/zephcore/helpers/TxtDataHelpers.h @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore TxtDataHelpers - text message type constants and string helpers + */ + +#pragma once + +#include +#include +#include + +#define TXT_TYPE_PLAIN 0 // a plain text message +#define TXT_TYPE_CLI_DATA 1 // a CLI command +#define TXT_TYPE_SIGNED_PLAIN 2 // plain text, signed by sender + +class StrHelper { +public: + static void strncpy(char *dest, const char *src, size_t buf_sz) { + if (buf_sz == 0) return; + size_t i = 0; + while (i < buf_sz - 1 && src[i] != '\0') { + dest[i] = src[i]; + i++; + } + dest[i] = '\0'; + } + + static void strzcpy(char *dest, const char *src, size_t buf_sz) { + if (buf_sz == 0) return; + size_t i = 0; + while (i < buf_sz - 1 && src[i] != '\0') { + dest[i] = src[i]; + i++; + } + while (i < buf_sz) { + dest[i++] = '\0'; + } + } + + static bool isBlank(const char *str) { + if (str == nullptr) return true; + while (*str) { + if (*str != ' ' && *str != '\t' && *str != '\n' && *str != '\r') return false; + str++; + } + return true; + } +}; diff --git a/zephcore/helpers/oled_power.c b/zephcore/helpers/oled_power.c new file mode 100644 index 0000000..a5b76f5 --- /dev/null +++ b/zephcore/helpers/oled_power.c @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore - OLED display power management + * + * Sends I2C display-off command to SH1106/SSD1306 OLED to save power. + * The Wio Tracker L1's SH1106 has no hardware power gate, so this is + * the only way to prevent it from drawing ~20mA in standby. + */ + +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_oled_power, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +#include "oled_power.h" + +#if IS_ENABLED(CONFIG_I2C) && DT_NODE_HAS_STATUS(DT_NODELABEL(i2c0), okay) + +#include + +#define OLED_I2C_ADDR 0x3D /* Wio Tracker L1 SH1106 address */ + +void oled_sleep(void) +{ + const struct device *i2c_dev = DEVICE_DT_GET(DT_NODELABEL(i2c0)); + if (!device_is_ready(i2c_dev)) { + return; + } + + uint8_t cmd[] = {0x00, 0xAE}; /* Display OFF */ + int ret = i2c_write(i2c_dev, cmd, sizeof(cmd), OLED_I2C_ADDR); + if (ret == 0) { + LOG_INF("OLED display put to sleep (power saving)"); + } else { + LOG_DBG("OLED sleep failed (ret=%d) — no OLED present", ret); + } +} + +#else + +void oled_sleep(void) { } + +#endif diff --git a/zephcore/helpers/oled_power.h b/zephcore/helpers/oled_power.h new file mode 100644 index 0000000..f402e40 --- /dev/null +++ b/zephcore/helpers/oled_power.h @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore - OLED display power management + * + * Sends I2C display-off command to SH1106/SSD1306 OLED to save power. + * No-op if I2C or OLED hardware is not present. + */ + +#ifndef ZEPHCORE_OLED_POWER_H +#define ZEPHCORE_OLED_POWER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Put OLED display to sleep (Display OFF command via I2C). + * Safe to call on any board — no-op when I2C or OLED is absent. + */ +void oled_sleep(void); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHCORE_OLED_POWER_H */ diff --git a/zephcore/helpers/ui/buzzer.c b/zephcore/helpers/ui/buzzer.c new file mode 100644 index 0000000..cf81d45 --- /dev/null +++ b/zephcore/helpers/ui/buzzer.c @@ -0,0 +1,468 @@ +/* + * ZephCore - PWM Buzzer with RTTTL Melody Playback + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Non-blocking RTTTL parser using Zephyr PWM API. + * Each note is scheduled via k_work_delayable - fully event-driven. + * + * RTTTL Format: "Name:d=D,o=O,b=B:note,note,..." + * D = default duration (1,2,4,8,16,32) + * O = default octave (4-7) + * B = tempo in BPM + * note = [duration][#][.][octave] + * pitch = c,d,e,f,g,a,b,p (p = pause/rest) + */ + +#include "buzzer.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(buzzer, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +/* ========== Note Frequency Table ========== */ +/* Frequencies for octave 4 (middle C = C4 = 262 Hz) */ +/* Index: C=0, C#=1, D=2, D#=3, E=4, F=5, F#=6, G=7, G#=8, A=9, A#=10, B=11 */ +static const uint16_t note_freq_o4[] = { + 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494 +}; + +/* ========== Buzzer Context ========== */ +struct buzzer_ctx { + struct pwm_dt_spec pwm; + const struct device *enable_reg; /* Optional regulator for buzzer amp */ + struct k_work_delayable note_work; + + /* RTTTL parser state */ + const char *melody; /* Current position in RTTTL string */ + uint16_t default_dur; /* Default note duration (1,2,4,8,16,32) */ + uint8_t default_oct; /* Default octave (4-7) */ + uint16_t bpm; /* Beats per minute */ + uint32_t whole_note_ms; /* Duration of a whole note in ms */ + + bool quiet; + bool playing; + bool initialized; +}; + +static struct buzzer_ctx ctx; + +/* ========== RTTTL Parser Helpers ========== */ + +static int parse_number(const char **p) +{ + int num = 0; + + while (**p >= '0' && **p <= '9') { + num = num * 10 + (**p - '0'); + (*p)++; + } + return num; +} + +static void skip_whitespace(const char **p) +{ + while (**p == ' ' || **p == '\t') { + (*p)++; + } +} + +/** + * Parse the RTTTL header: "Name:d=D,o=O,b=B:" + * Sets defaults and advances pointer past the second colon. + */ +static bool parse_header(const char *rtttl) +{ + const char *p = rtttl; + + /* Skip name (everything before first ':') */ + while (*p && *p != ':') { + p++; + } + if (!*p) { + return false; + } + p++; /* skip ':' */ + + /* Parse default values section */ + ctx.default_dur = 4; + ctx.default_oct = 6; + ctx.bpm = 63; + + while (*p && *p != ':') { + skip_whitespace(&p); + char key = tolower((unsigned char)*p); + + p++; + if (*p == '=') { + p++; + } + + int val = parse_number(&p); + + switch (key) { + case 'd': + ctx.default_dur = val; + break; + case 'o': + ctx.default_oct = val; + break; + case 'b': + ctx.bpm = val; + break; + } + + if (*p == ',') { + p++; + } + } + + if (!*p) { + return false; + } + p++; /* skip second ':' */ + + ctx.melody = p; + /* Whole note = 4 beats. At B bpm, one beat = 60000/B ms */ + ctx.whole_note_ms = (60000UL * 4) / ctx.bpm; + + return true; +} + +/** + * Parse the next note from the melody string. + * Returns frequency in Hz (0 for rest) and duration in ms. + * Returns false if no more notes. + */ +static bool parse_next_note(uint16_t *freq_hz, uint32_t *dur_ms) +{ + const char *p = ctx.melody; + + skip_whitespace(&p); + + if (!*p) { + return false; + } + + /* Parse optional duration */ + uint16_t dur = parse_number(&p); + + if (dur == 0) { + dur = ctx.default_dur; + } + + /* Parse note letter */ + skip_whitespace(&p); + char note = tolower((unsigned char)*p); + + if (!note) { + return false; + } + p++; + + /* Map note letter to semitone index */ + int semitone = -1; + bool is_rest = false; + + switch (note) { + case 'c': semitone = 0; break; + case 'd': semitone = 2; break; + case 'e': semitone = 4; break; + case 'f': semitone = 5; break; + case 'g': semitone = 7; break; + case 'a': semitone = 9; break; + case 'b': semitone = 11; break; + case 'p': is_rest = true; break; + default: + /* Unknown note, skip */ + while (*p && *p != ',' && *p != ':') { + p++; + } + if (*p == ',') { + p++; + } + ctx.melody = p; + *freq_hz = 0; + *dur_ms = ctx.whole_note_ms / dur; + return true; + } + + /* Check for sharp (#) */ + if (*p == '#') { + semitone++; + p++; + } + + /* Check for dotted note */ + bool dotted = false; + + if (*p == '.') { + dotted = true; + p++; + } + + /* Parse optional octave */ + uint8_t oct = 0; + if (*p >= '0' && *p <= '9') { + oct = parse_number(&p); + } else { + oct = ctx.default_oct; + } + + /* Check for dotted note after octave too */ + if (*p == '.') { + dotted = true; + p++; + } + + /* Calculate duration */ + uint32_t note_dur = ctx.whole_note_ms / dur; + + if (dotted) { + note_dur += note_dur / 2; + } + *dur_ms = note_dur; + + /* Calculate frequency */ + if (is_rest) { + *freq_hz = 0; + } else { + /* Base freq from octave 4, shift by octave difference */ + uint32_t f = note_freq_o4[semitone % 12]; + int oct_diff = (int)oct - 4; + + if (oct_diff > 0) { + f <<= oct_diff; + } else if (oct_diff < 0) { + f >>= (-oct_diff); + } + *freq_hz = (uint16_t)f; + } + + /* Skip comma separator */ + if (*p == ',') { + p++; + } + ctx.melody = p; + + return true; +} + +/* ========== Amplifier Enable/Disable ========== */ + +static void buzzer_amp_on(void) +{ + if (ctx.enable_reg) { + regulator_enable(ctx.enable_reg); + } +} + +static void buzzer_amp_off(void) +{ + if (ctx.enable_reg) { + regulator_disable(ctx.enable_reg); + } +} + +/* ========== PWM Control ========== */ + +static void buzzer_set_tone(uint16_t freq_hz) +{ + if (!ctx.initialized) { + return; + } + + if (freq_hz == 0) { + /* Silence - set duty cycle to 0 */ + pwm_set_dt(&ctx.pwm, PWM_HZ(1000), 0); + } else { + /* Set PWM to desired frequency with 50% duty cycle */ + uint32_t period_ns = 1000000000UL / freq_hz; + pwm_set_dt(&ctx.pwm, period_ns, period_ns / 2); + } +} + +static void buzzer_silence(void) +{ + buzzer_set_tone(0); +} + +/* ========== Note Work Handler ========== */ + +static void note_work_handler(struct k_work *work) +{ + uint16_t freq; + uint32_t dur_ms; + + if (!ctx.playing) { + buzzer_silence(); + buzzer_amp_off(); + return; + } + + if (!parse_next_note(&freq, &dur_ms)) { + /* Melody complete */ + buzzer_silence(); + buzzer_amp_off(); + ctx.playing = false; + return; + } + + /* Play this note */ + buzzer_set_tone(freq); + + /* Add a small gap between notes (90% tone, 10% silence) */ + uint32_t tone_ms = (dur_ms * 9) / 10; + uint32_t gap_ms = dur_ms - tone_ms; + + if (gap_ms < 1) { + gap_ms = 1; + } + + /* Schedule silence gap after tone, then next note */ + /* For simplicity: play tone for full duration, next work picks up next note. + * The inter-note gap comes from the 90/10 split. */ + (void)gap_ms; + k_work_reschedule(&ctx.note_work, K_MSEC(dur_ms)); +} + +/* ========== Public API ========== */ + +int buzzer_init(void) +{ + /* Check for buzzer alias in devicetree */ + const struct device *pwm_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(pwm0)); + + if (!pwm_dev || !device_is_ready(pwm_dev)) { + LOG_INF("no PWM device found - buzzer disabled"); + return -ENODEV; + } + + /* Get PWM spec from the buzzer node via alias */ + if (!DT_HAS_ALIAS(buzzer)) { + LOG_INF("no buzzer alias in DT - buzzer disabled"); + return -ENODEV; + } + + /* PWM spec from the pwm-leds buzzer node */ + ctx.pwm = (struct pwm_dt_spec)PWM_DT_SPEC_GET(DT_ALIAS(buzzer)); + + if (!pwm_is_ready_dt(&ctx.pwm)) { + LOG_ERR("PWM device not ready"); + return -ENODEV; + } + + /* Check for optional buzzer enable regulator (power gate for amplifier) */ + ctx.enable_reg = NULL; + if (DT_HAS_ALIAS(buzzer_enable)) { + const struct device *en_dev = DEVICE_DT_GET_OR_NULL(DT_ALIAS(buzzer_enable)); + if (en_dev && device_is_ready(en_dev)) { + ctx.enable_reg = en_dev; + LOG_INF("buzzer enable regulator detected"); + } + } + + k_work_init_delayable(&ctx.note_work, note_work_handler); + + ctx.quiet = true; /* Start quiet like Arduino */ + ctx.playing = false; + ctx.initialized = true; + + /* Ensure buzzer is silent and amp off on init */ + buzzer_silence(); + buzzer_amp_off(); + + LOG_INF("buzzer initialized (PWM)"); + return 0; +} + +void buzzer_play(const char *rtttl) +{ + if (!ctx.initialized) { + return; + } + + /* Stop any current melody */ + if (ctx.playing) { + buzzer_stop(); + } + + if (ctx.quiet) { + return; + } + + if (!rtttl || !*rtttl) { + return; + } + + /* Parse RTTTL header */ + if (!parse_header(rtttl)) { + LOG_WRN("invalid RTTTL format"); + return; + } + + ctx.playing = true; + + /* Enable buzzer amplifier power */ + buzzer_amp_on(); + + /* Start playing first note immediately */ + k_work_reschedule(&ctx.note_work, K_NO_WAIT); +} + +void buzzer_stop(void) +{ + if (!ctx.initialized) { + return; + } + + ctx.playing = false; + k_work_cancel_delayable(&ctx.note_work); + buzzer_silence(); + buzzer_amp_off(); +} + +void buzzer_set_quiet(bool quiet) +{ + ctx.quiet = quiet; + + if (quiet) { + if (ctx.playing) { + buzzer_stop(); + } else { + /* Ensure amp is off even if not playing */ + buzzer_silence(); + buzzer_amp_off(); + } + } + + LOG_INF("buzzer %s", quiet ? "muted" : "enabled"); +} + +void buzzer_set_quiet_deferred(bool quiet) +{ + /* Set the flag but don't stop the current melody. + * The melody will play out via note_work_handler (which doesn't + * check quiet). Future buzzer_play() calls will check ctx.quiet + * and become no-ops. */ + ctx.quiet = quiet; + LOG_INF("buzzer %s (deferred)", quiet ? "muted" : "enabled"); +} + +bool buzzer_is_quiet(void) +{ + return ctx.quiet; +} + +bool buzzer_is_playing(void) +{ + return ctx.playing; +} diff --git a/zephcore/helpers/ui/buzzer.h b/zephcore/helpers/ui/buzzer.h new file mode 100644 index 0000000..63d6f90 --- /dev/null +++ b/zephcore/helpers/ui/buzzer.h @@ -0,0 +1,93 @@ +/* + * ZephCore - PWM Buzzer with RTTTL Melody Playback + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Non-blocking RTTTL melody playback using Zephyr PWM API. + * Notes are scheduled via k_work_delayable - no polling needed. + * + * Usage: + * buzzer_init(); // Auto-detect from DT aliases + * buzzer_play(MELODY_STARTUP); // Start melody (non-blocking) + * buzzer_stop(); // Stop immediately + * buzzer_set_quiet(true); // Mute + * buzzer_is_playing(); // Check if melody active + */ + +#ifndef ZEPHCORE_BUZZER_H +#define ZEPHCORE_BUZZER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ========== Predefined RTTTL Melodies ========== */ +/* Format: "Name:d=duration,o=octave,b=bpm:notes..." */ + +#define MELODY_STARTUP "Startup:d=4,o=5,b=160:16c6,16e6,8g6" +#define MELODY_SHUTDOWN "Shutdown:d=4,o=5,b=100:8g5,16e5,16c5" +#define MELODY_MSG_CONTACT "MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7" +#define MELODY_MSG_CHANNEL "kerplop:d=16,o=6,b=120:32g#,32c#" +#define MELODY_ACK "ack:d=32,o=8,b=120:c" + +/* ========== Public API ========== */ + +/** + * Initialize buzzer from devicetree. + * Looks for 'buzzer' alias in DT → pwm-leds node. + * Also checks for optional 'buzzer-enable' alias for power gating. + * + * @return 0 on success, negative errno on failure, -ENODEV if no buzzer in DT + */ +int buzzer_init(void); + +/** + * Play an RTTTL melody string. Non-blocking - returns immediately. + * If a melody is already playing, it is stopped first. + * If buzzer is in quiet mode, this is a no-op. + * + * @param rtttl RTTTL format melody string (must remain valid until done) + */ +void buzzer_play(const char *rtttl); + +/** + * Stop any playing melody immediately and silence the buzzer. + */ +void buzzer_stop(void); + +/** + * Set quiet mode. When quiet, buzzer_play() is a no-op. + * Also controls the optional enable pin (power gate). + * + * @param quiet true to mute, false to enable + */ +void buzzer_set_quiet(bool quiet); + +/** + * @return true if buzzer is in quiet mode + */ +bool buzzer_is_quiet(void); + +/** + * Set quiet mode without stopping a currently playing melody. + * Unlike buzzer_set_quiet(), this allows the in-progress melody + * to finish playing. Future buzzer_play() calls will be suppressed. + * Useful for "mute" feedback where you want the confirmation sound + * to play out before silence takes effect. + * + * @param quiet true to mute (after current melody), false to enable + */ +void buzzer_set_quiet_deferred(bool quiet); + +/** + * @return true if a melody is currently playing + */ +bool buzzer_is_playing(void); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHCORE_BUZZER_H */ diff --git a/zephcore/helpers/ui/cfb_font_0608.c b/zephcore/helpers/ui/cfb_font_0608.c new file mode 100644 index 0000000..27604b5 --- /dev/null +++ b/zephcore/helpers/ui/cfb_font_0608.c @@ -0,0 +1,925 @@ +/* + * ZephCore - 6x8 monospace font for CFB + * Classic CP437-style 6x8 bitmap font, VPACKED (column-major, LSB top). + * Each glyph is 6 bytes wide x 8 bits tall. + * Covers code points 32-255 (ASCII + Latin-1 Supplement). + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +static const uint8_t cfb_font_0608[224][6] = { + /* === ASCII 32-126 (standard printable characters) === */ + + /* 32 (space) */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + /* 33 (!) */ + { 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00 }, + /* 34 (") */ + { 0x00, 0x07, 0x00, 0x07, 0x00, 0x00 }, + /* 35 (#) */ + { 0x14, 0x7F, 0x14, 0x7F, 0x14, 0x00 }, + /* 36 ($) */ + { 0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x00 }, + /* 37 (%) */ + { 0x23, 0x13, 0x08, 0x64, 0x62, 0x00 }, + /* 38 (&) */ + { 0x36, 0x49, 0x55, 0x22, 0x50, 0x00 }, + /* 39 (') */ + { 0x00, 0x05, 0x03, 0x00, 0x00, 0x00 }, + /* 40 (() */ + { 0x00, 0x1C, 0x22, 0x41, 0x00, 0x00 }, + /* 41 ()) */ + { 0x00, 0x41, 0x22, 0x1C, 0x00, 0x00 }, + /* 42 (*) */ + { 0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00 }, + /* 43 (+) */ + { 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00 }, + /* 44 (,) */ + { 0x00, 0x50, 0x30, 0x00, 0x00, 0x00 }, + /* 45 (-) */ + { 0x08, 0x08, 0x08, 0x08, 0x08, 0x00 }, + /* 46 (.) */ + { 0x00, 0x60, 0x60, 0x00, 0x00, 0x00 }, + /* 47 (/) */ + { 0x20, 0x10, 0x08, 0x04, 0x02, 0x00 }, + /* 48 (0) */ + { 0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00 }, + /* 49 (1) */ + { 0x00, 0x42, 0x7F, 0x40, 0x00, 0x00 }, + /* 50 (2) */ + { 0x42, 0x61, 0x51, 0x49, 0x46, 0x00 }, + /* 51 (3) */ + { 0x21, 0x41, 0x45, 0x4B, 0x31, 0x00 }, + /* 52 (4) */ + { 0x18, 0x14, 0x12, 0x7F, 0x10, 0x00 }, + /* 53 (5) */ + { 0x27, 0x45, 0x45, 0x45, 0x39, 0x00 }, + /* 54 (6) */ + { 0x3C, 0x4A, 0x49, 0x49, 0x30, 0x00 }, + /* 55 (7) */ + { 0x01, 0x71, 0x09, 0x05, 0x03, 0x00 }, + /* 56 (8) */ + { 0x36, 0x49, 0x49, 0x49, 0x36, 0x00 }, + /* 57 (9) */ + { 0x06, 0x49, 0x49, 0x29, 0x1E, 0x00 }, + /* 58 (:) */ + { 0x00, 0x36, 0x36, 0x00, 0x00, 0x00 }, + /* 59 (;) */ + { 0x00, 0x56, 0x36, 0x00, 0x00, 0x00 }, + /* 60 (<) */ + { 0x00, 0x08, 0x14, 0x22, 0x41, 0x00 }, + /* 61 (=) */ + { 0x14, 0x14, 0x14, 0x14, 0x14, 0x00 }, + /* 62 (>) */ + { 0x41, 0x22, 0x14, 0x08, 0x00, 0x00 }, + /* 63 (?) */ + { 0x02, 0x01, 0x51, 0x09, 0x06, 0x00 }, + /* 64 (@) */ + { 0x32, 0x49, 0x79, 0x41, 0x3E, 0x00 }, + /* 65 (A) */ + { 0x7E, 0x11, 0x11, 0x11, 0x7E, 0x00 }, + /* 66 (B) */ + { 0x7F, 0x49, 0x49, 0x49, 0x36, 0x00 }, + /* 67 (C) */ + { 0x3E, 0x41, 0x41, 0x41, 0x22, 0x00 }, + /* 68 (D) */ + { 0x7F, 0x41, 0x41, 0x22, 0x1C, 0x00 }, + /* 69 (E) */ + { 0x7F, 0x49, 0x49, 0x49, 0x41, 0x00 }, + /* 70 (F) */ + { 0x7F, 0x09, 0x09, 0x01, 0x01, 0x00 }, + /* 71 (G) */ + { 0x3E, 0x41, 0x41, 0x51, 0x32, 0x00 }, + /* 72 (H) */ + { 0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00 }, + /* 73 (I) */ + { 0x00, 0x41, 0x7F, 0x41, 0x00, 0x00 }, + /* 74 (J) */ + { 0x20, 0x40, 0x41, 0x3F, 0x01, 0x00 }, + /* 75 (K) */ + { 0x7F, 0x08, 0x14, 0x22, 0x41, 0x00 }, + /* 76 (L) */ + { 0x7F, 0x40, 0x40, 0x40, 0x40, 0x00 }, + /* 77 (M) */ + { 0x7F, 0x02, 0x04, 0x02, 0x7F, 0x00 }, + /* 78 (N) */ + { 0x7F, 0x04, 0x08, 0x10, 0x7F, 0x00 }, + /* 79 (O) */ + { 0x3E, 0x41, 0x41, 0x41, 0x3E, 0x00 }, + /* 80 (P) */ + { 0x7F, 0x09, 0x09, 0x09, 0x06, 0x00 }, + /* 81 (Q) */ + { 0x3E, 0x41, 0x51, 0x21, 0x5E, 0x00 }, + /* 82 (R) */ + { 0x7F, 0x09, 0x19, 0x29, 0x46, 0x00 }, + /* 83 (S) */ + { 0x46, 0x49, 0x49, 0x49, 0x31, 0x00 }, + /* 84 (T) */ + { 0x01, 0x01, 0x7F, 0x01, 0x01, 0x00 }, + /* 85 (U) */ + { 0x3F, 0x40, 0x40, 0x40, 0x3F, 0x00 }, + /* 86 (V) */ + { 0x1F, 0x20, 0x40, 0x20, 0x1F, 0x00 }, + /* 87 (W) */ + { 0x7F, 0x20, 0x18, 0x20, 0x7F, 0x00 }, + /* 88 (X) */ + { 0x63, 0x14, 0x08, 0x14, 0x63, 0x00 }, + /* 89 (Y) */ + { 0x03, 0x04, 0x78, 0x04, 0x03, 0x00 }, + /* 90 (Z) */ + { 0x61, 0x51, 0x49, 0x45, 0x43, 0x00 }, + /* 91 ([) */ + { 0x00, 0x00, 0x7F, 0x41, 0x41, 0x00 }, + /* 92 (\) */ + { 0x02, 0x04, 0x08, 0x10, 0x20, 0x00 }, + /* 93 (]) */ + { 0x41, 0x41, 0x7F, 0x00, 0x00, 0x00 }, + /* 94 (^) */ + { 0x04, 0x02, 0x01, 0x02, 0x04, 0x00 }, + /* 95 (_) */ + { 0x40, 0x40, 0x40, 0x40, 0x40, 0x00 }, + /* 96 (`) */ + { 0x00, 0x01, 0x02, 0x04, 0x00, 0x00 }, + /* 97 (a) */ + { 0x20, 0x54, 0x54, 0x54, 0x78, 0x00 }, + /* 98 (b) */ + { 0x7F, 0x48, 0x44, 0x44, 0x38, 0x00 }, + /* 99 (c) */ + { 0x38, 0x44, 0x44, 0x44, 0x20, 0x00 }, + /* 100 (d) */ + { 0x38, 0x44, 0x44, 0x48, 0x7F, 0x00 }, + /* 101 (e) */ + { 0x38, 0x54, 0x54, 0x54, 0x18, 0x00 }, + /* 102 (f) */ + { 0x08, 0x7E, 0x09, 0x01, 0x02, 0x00 }, + /* 103 (g) */ + { 0x08, 0x14, 0x54, 0x54, 0x3C, 0x00 }, + /* 104 (h) */ + { 0x7F, 0x08, 0x04, 0x04, 0x78, 0x00 }, + /* 105 (i) */ + { 0x00, 0x44, 0x7D, 0x40, 0x00, 0x00 }, + /* 106 (j) */ + { 0x20, 0x40, 0x44, 0x3D, 0x00, 0x00 }, + /* 107 (k) */ + { 0x00, 0x7F, 0x10, 0x28, 0x44, 0x00 }, + /* 108 (l) */ + { 0x00, 0x41, 0x7F, 0x40, 0x00, 0x00 }, + /* 109 (m) */ + { 0x7C, 0x04, 0x18, 0x04, 0x78, 0x00 }, + /* 110 (n) */ + { 0x7C, 0x08, 0x04, 0x04, 0x78, 0x00 }, + /* 111 (o) */ + { 0x38, 0x44, 0x44, 0x44, 0x38, 0x00 }, + /* 112 (p) */ + { 0x7C, 0x14, 0x14, 0x14, 0x08, 0x00 }, + /* 113 (q) */ + { 0x08, 0x14, 0x14, 0x18, 0x7C, 0x00 }, + /* 114 (r) */ + { 0x7C, 0x08, 0x04, 0x04, 0x08, 0x00 }, + /* 115 (s) */ + { 0x48, 0x54, 0x54, 0x54, 0x20, 0x00 }, + /* 116 (t) */ + { 0x04, 0x3F, 0x44, 0x40, 0x20, 0x00 }, + /* 117 (u) */ + { 0x3C, 0x40, 0x40, 0x20, 0x7C, 0x00 }, + /* 118 (v) */ + { 0x1C, 0x20, 0x40, 0x20, 0x1C, 0x00 }, + /* 119 (w) */ + { 0x3C, 0x40, 0x30, 0x40, 0x3C, 0x00 }, + /* 120 (x) */ + { 0x44, 0x28, 0x10, 0x28, 0x44, 0x00 }, + /* 121 (y) */ + { 0x0C, 0x50, 0x50, 0x50, 0x3C, 0x00 }, + /* 122 (z) */ + { 0x44, 0x64, 0x54, 0x4C, 0x44, 0x00 }, + /* 123 ({) */ + { 0x00, 0x08, 0x36, 0x41, 0x00, 0x00 }, + /* 124 (|) */ + { 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00 }, + /* 125 (}) */ + { 0x00, 0x41, 0x36, 0x08, 0x00, 0x00 }, + /* 126 (~) */ + { 0x08, 0x08, 0x2A, 0x1C, 0x08, 0x00 }, + + /* === Code points 127-159 (DEL + C1 control codes) === */ + + /* 127 (DEL) - blank */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + /* 128-159: C1 control codes - blank */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 128 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 129 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 130 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 131 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 132 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 133 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 134 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 135 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 136 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 137 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 138 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 139 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 140 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 141 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 142 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 143 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 144 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 145 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 146 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 147 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 148 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 149 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 150 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 151 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 152 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 153 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 154 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 155 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 156 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 157 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 158 */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 159 */ + + /* === Latin-1 Supplement 160-255 === */ + + /* 160 (NBSP - non-breaking space) */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + + /* 161 inverted exclamation + * . + * + * # + * # + * # + * # + * # + * . + */ + { 0x00, 0x00, 0xFD, 0x00, 0x00, 0x00 }, + + /* 162 cent sign + * . . . . . + * . . # . . + * . # # # . + * # . # . # + * # . # . . + * . # . # . + * . . . . . + * . . . . . + */ + { 0x00, 0x38, 0x54, 0x7C, 0x54, 0x18 }, + + /* 163 pound sign + * . # # . . + * # . . # . + * # # # # . + * . # . . . + * # # # # . + * . . . . . + * . . . . . + * . . . . . + * + * Row 0: .#.#. Row 1: #..#. Row 2: ####. Row 3: .#... Row 4: ####. + * Actually standard pound: top arc, crossbar through stem + */ + { 0x08, 0x3E, 0x09, 0x09, 0x12, 0x00 }, + + /* 164 currency sign + * # . . . # + * . # # # . + * . # . # . + * . # # # . + * # . . . # + * . . . . . + */ + { 0x11, 0x0E, 0x0A, 0x0E, 0x11, 0x00 }, + + /* 165 yen sign + * # . . . # + * . # . # . + * # # # # # + * . . # . . + * # # # # # + * . . # . . + * . . . . . + * . . . . . + */ + { 0x05, 0x0A, 0x3E, 0x0A, 0x05, 0x00 }, + + /* 166 broken bar + * . . # . . + * . . # . . + * . . # . . + * . . . . . + * . . # . . + * . . # . . + * . . # . . + * . . . . . + */ + { 0x00, 0x00, 0x77, 0x00, 0x00, 0x00 }, + + /* 167 section sign + * . . # # . + * . # . # . + * . # # . . + * . . # # . + * . # . # . + * . # # . . + * . . . . . + * . . . . . + */ + { 0x00, 0x26, 0x49, 0x49, 0x32, 0x00 }, + + /* 168 diaeresis (two dots) + * . # . # . + * . . . . . + * . . . . . + * . . . . . + * . . . . . + * . . . . . + */ + { 0x00, 0x01, 0x00, 0x01, 0x00, 0x00 }, + + /* 169 copyright sign + * . # # # . + * # . . . # + * # . # . # + * # # . # # + * # . # . # + * . # # # . + * . . . . . + * . . . . . + * + * C inside circle + */ + { 0x1C, 0x22, 0x2A, 0x2A, 0x1C, 0x00 }, + + /* 170 feminine ordinal indicator (superscript a with underline) + * . # # . . + * # . # . . + * . # # . . + * . . . . . + * # # # . . + * . . . . . + */ + { 0x04, 0x15, 0x15, 0x0E, 0x00, 0x00 }, + + /* 171 left guillemet << + * . . . . . + * . # . # . + * # . # . . + * . # . # . + * . . . . . + * . . . . . + */ + { 0x08, 0x14, 0x08, 0x14, 0x00, 0x00 }, + + /* 172 not sign + * . . . . . + * . . . . . + * # # # # . + * . . . # . + * . . . # . + * . . . . . + */ + { 0x04, 0x04, 0x04, 0x0C, 0x00, 0x00 }, + + /* 173 soft hyphen (dash) + * . . . . . + * . . . . . + * . . . . . + * . # # # . + * . . . . . + * . . . . . + */ + { 0x00, 0x08, 0x08, 0x08, 0x00, 0x00 }, + + /* 174 registered sign + * . # # # . + * # . . . # + * # . # . # + * # . . . # + * # . # . # + * . # # # . + * . . . . . + * . . . . . + * + * R inside circle + */ + { 0x1C, 0x22, 0x2E, 0x2A, 0x1C, 0x00 }, + + /* 175 macron (overline) + * # # # # # + * . . . . . + * . . . . . + * . . . . . + * . . . . . + * . . . . . + */ + { 0x01, 0x01, 0x01, 0x01, 0x01, 0x00 }, + + /* 176 degree sign + * . . # . . + * . # . # . + * . . # . . + * . . . . . + * . . . . . + * . . . . . + */ + { 0x00, 0x02, 0x05, 0x02, 0x00, 0x00 }, + + /* 177 plus-minus + * . . # . . + * . # # # . + * . . # . . + * . . . . . + * . # # # . + * . . . . . + */ + { 0x00, 0x24, 0x2E, 0x24, 0x00, 0x00 }, + + /* 178 superscript 2 + * . # . . + * # . # . + * . # . . + * # # # . + * . . . . + * . . . . + */ + { 0x09, 0x05, 0x03, 0x00, 0x00, 0x00 }, + + /* 179 superscript 3 + * # # # . + * . . # . + * . # # . + * # # # . + * . . . . + * . . . . + */ + { 0x05, 0x05, 0x03, 0x00, 0x00, 0x00 }, + + /* 180 acute accent + * . . . . . + * . . # . . + * . # . . . + * . . . . . + * . . . . . + * . . . . . + */ + { 0x00, 0x00, 0x02, 0x01, 0x00, 0x00 }, + + /* 181 micro sign (mu) + * . . . . . + * . . . . . + * # . . . # + * # . . . # + * # . . # # + * # # # . . + * # . . . . + * . . . . . + */ + { 0xFC, 0x40, 0x40, 0x20, 0x7C, 0x00 }, + + /* 182 pilcrow (paragraph sign) + * . # # # # + * # # . . # + * # # . . # + * . # . . # + * . . . . # + * . . . . # + * . . . . . + * . . . . . + */ + { 0x06, 0x0F, 0x09, 0x7F, 0x09, 0x00 }, + + /* 183 middle dot + * . . . . . + * . . . . . + * . . # . . + * . . # . . + * . . . . . + * . . . . . + */ + { 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00 }, + + /* 184 cedilla + * . . . . . + * . . . . . + * . . . . . + * . . . . . + * . . . . . + * . . . . . + * . . # . . + * . # . . . + */ + { 0x00, 0x80, 0x40, 0x00, 0x00, 0x00 }, + + /* 185 superscript 1 + * . # . . + * # # . . + * . # . . + * # # # . + * . . . . + * . . . . + */ + { 0x00, 0x09, 0x0F, 0x08, 0x00, 0x00 }, + + /* 186 masculine ordinal indicator (superscript o with underline) + * . # # . . + * # . . # . + * . # # . . + * . . . . . + * # # # # . + * . . . . . + */ + { 0x06, 0x09, 0x09, 0x06, 0x00, 0x00 }, + + /* 187 right guillemet >> + * . . . . . + * # . # . . + * . # . # . + * # . # . . + * . . . . . + * . . . . . + */ + { 0x00, 0x14, 0x08, 0x14, 0x08, 0x00 }, + + /* 188 fraction 1/4 + * # . . . . + * # . . # . + * . . # . . + * . # . . . + * # . # # . + * . . . # . + * . . # # # + * . . . . . + */ + { 0x0F, 0x10, 0x08, 0x74, 0x42, 0x7E }, + + /* 189 fraction 1/2 + * # . . . . + * # . . # . + * . . # . . + * . # . . . + * # . # . # + * . . . # . + * . . # # # + * . . . . . + */ + { 0x0F, 0x10, 0x08, 0x54, 0x52, 0x4E }, + + /* 190 fraction 3/4 + * # # . . . + * . # . . # + * # # # . . + * . . . # . + * . . # # . + * . . . # . + * . . # # # + * . . . . . + */ + { 0x05, 0x17, 0x08, 0x74, 0x42, 0x7E }, + + /* 191 inverted question mark + * . . . . . + * . . # . . + * . . . . . + * . . # . . + * . # . . . + * # . . . # + * . # # # . + * . . . . . + */ + { 0x30, 0x48, 0x44, 0x40, 0x20, 0x00 }, + + /* + * === UPPERCASE ACCENTED LETTERS (192-222) === + * + * Strategy: Shift base letter down 1 pixel (each byte <<1) to free row 0 + * for the accent mark. Base letters use rows 0-6 (bits 0-6); after shift + * they occupy rows 1-7 (bits 1-7) for edge columns, but inner columns + * that only had bits 0 and 4 (like A's col1 = 0x11) shift to bits 1 and 5. + * This means row 0 (bit 0) is free across all columns. + * + * Accent patterns on row 0 (bit 0 = 0x01): + * Grave: col1 |= 0x01 (dot left of center) + * Acute: col3 |= 0x01 (dot right of center) + * Circumflex: col2 |= 0x01 (dot at center) + * Tilde: col1,col3 |= 0x01 (two dots) + * Diaeresis: col1,col3 |= 0x01 (two dots, same as tilde at this res) + * Ring above: col1,col2,col3 |= 0x01 (three dots = line) + * + * Shifted bases (each byte of original << 1): + * A: { 0xFC, 0x22, 0x22, 0x22, 0xFC } + * E: { 0xFE, 0x92, 0x92, 0x92, 0x82 } + * I: { 0x00, 0x82, 0xFE, 0x82, 0x00 } + * O: { 0x7C, 0x82, 0x82, 0x82, 0x7C } + * U: { 0x7E, 0x80, 0x80, 0x80, 0x7E } + * N: { 0xFE, 0x08, 0x10, 0x20, 0xFE } + * Y: { 0x06, 0x08, 0xF0, 0x08, 0x06 } + */ + + /* 192 A grave */ + { 0xFC, 0x23, 0x22, 0x22, 0xFC, 0x00 }, + /* 193 A acute */ + { 0xFC, 0x22, 0x22, 0x23, 0xFC, 0x00 }, + /* 194 A circumflex */ + { 0xFC, 0x22, 0x23, 0x22, 0xFC, 0x00 }, + /* 195 A tilde */ + { 0xFC, 0x23, 0x22, 0x23, 0xFC, 0x00 }, + /* 196 A diaeresis */ + { 0xFC, 0x23, 0x22, 0x23, 0xFC, 0x00 }, + /* 197 A ring above */ + { 0xFC, 0x23, 0x23, 0x23, 0xFC, 0x00 }, + + /* 198 AE ligature + * . # # # # # + * # . # . . . + * # . # # # . + * # # # . . . + * # . # . . . + * # . # # # # + * . . . . . . + * . . . . . . + */ + { 0x7E, 0x09, 0x7F, 0x49, 0x49, 0x41 }, + + /* 199 C cedilla + * Standard C with cedilla below (hook at bottom) + * C = { 0x3E, 0x41, 0x41, 0x41, 0x22 } + * Add cedilla: col2 or col3 gets bit 7 (row 7) extra + */ + { 0x3E, 0x41, 0x41, 0xC1, 0x22, 0x00 }, + + /* 200 E grave */ + { 0xFE, 0x93, 0x92, 0x92, 0x82, 0x00 }, + /* 201 E acute */ + { 0xFE, 0x92, 0x92, 0x93, 0x82, 0x00 }, + /* 202 E circumflex */ + { 0xFE, 0x92, 0x93, 0x92, 0x82, 0x00 }, + /* 203 E diaeresis */ + { 0xFE, 0x93, 0x92, 0x93, 0x82, 0x00 }, + + /* 204 I grave */ + { 0x00, 0x83, 0xFE, 0x82, 0x00, 0x00 }, + /* 205 I acute */ + { 0x00, 0x82, 0xFE, 0x83, 0x00, 0x00 }, + /* 206 I circumflex */ + { 0x00, 0x82, 0xFF, 0x82, 0x00, 0x00 }, + /* 207 I diaeresis */ + { 0x00, 0x83, 0xFE, 0x83, 0x00, 0x00 }, + + /* 208 Eth (D with stroke) + * . # # # . . + * . # . . # . + * # # # . . # + * . # . . . # + * . # . . # . + * . . # # . . + * . . . . . . + * . . . . . . + * + * D = { 0x7F, 0x41, 0x41, 0x22, 0x1C } + * Add crossbar: OR 0x08 onto appropriate columns + */ + { 0x04, 0x7F, 0x49, 0x41, 0x22, 0x1C }, + + /* 209 N tilde */ + { 0xFE, 0x09, 0x10, 0x21, 0xFE, 0x00 }, + + /* 210 O grave */ + { 0x7C, 0x83, 0x82, 0x82, 0x7C, 0x00 }, + /* 211 O acute */ + { 0x7C, 0x82, 0x82, 0x83, 0x7C, 0x00 }, + /* 212 O circumflex */ + { 0x7C, 0x82, 0x83, 0x82, 0x7C, 0x00 }, + /* 213 O tilde */ + { 0x7C, 0x83, 0x82, 0x83, 0x7C, 0x00 }, + /* 214 O diaeresis */ + { 0x7C, 0x83, 0x82, 0x83, 0x7C, 0x00 }, + + /* 215 multiplication sign (x) + * . . . . . + * . . . . . + * . # . # . + * . . # . . + * . # . # . + * . . . . . + * . . . . . + * . . . . . + */ + { 0x00, 0x14, 0x08, 0x14, 0x00, 0x00 }, + + /* 216 O stroke (O with /) */ + { 0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00 }, + + /* 217 U grave */ + { 0x7E, 0x81, 0x80, 0x80, 0x7E, 0x00 }, + /* 218 U acute */ + { 0x7E, 0x80, 0x80, 0x81, 0x7E, 0x00 }, + /* 219 U circumflex */ + { 0x7E, 0x80, 0x81, 0x80, 0x7E, 0x00 }, + /* 220 U diaeresis */ + { 0x7E, 0x81, 0x80, 0x81, 0x7E, 0x00 }, + + /* 221 Y acute */ + { 0x06, 0x08, 0xF0, 0x09, 0x06, 0x00 }, + + /* 222 Thorn (P-like with descender) + * # . . . . + * # # # # . + * # . . . # + * # . . . # + * # # # # . + * # . . . . + * . . . . . + * . . . . . + */ + { 0x7F, 0x14, 0x14, 0x14, 0x08, 0x00 }, + + /* 223 sharp s (eszett) + * . # # . . + * # . . # . + * # . # . . + * # . . # . + * # . . . # + * # . . # . + * . . . . . + * . . . . . + * + * Classic eszett: like a B but with the top loop different + */ + { 0x7E, 0x09, 0x09, 0x16, 0x20, 0x00 }, + + /* + * === LOWERCASE ACCENTED LETTERS (224-255) === + * + * Lowercase letters typically occupy rows 2-6 (bits 2-6), leaving + * rows 0-1 (bits 0-1) free for accents. No shifting needed! + * + * 2-row accent patterns (rows 0-1): + * Grave: col1 |= 0x02 (row1), col2 |= 0x01 (row0) -> \ shape + * Acute: col2 |= 0x01 (row0), col3 |= 0x02 (row1) -> / shape + * Circumflex: col1 |= 0x02, col2 |= 0x01, col3 |= 0x02 -> ^ shape + * Tilde: col1 |= 0x01, col2 |= 0x02, col3 |= 0x01 -> ~ shape + * Diaeresis: col1 |= 0x01, col3 |= 0x01 -> .. dots + * Ring above: col1 |= 0x02, col2 |= 0x01, col3 |= 0x02 -> same as ^ + */ + + /* 224 a grave + * a = { 0x20, 0x54, 0x54, 0x54, 0x78 } + */ + { 0x20, 0x56, 0x55, 0x54, 0x78, 0x00 }, + /* 225 a acute */ + { 0x20, 0x54, 0x55, 0x56, 0x78, 0x00 }, + /* 226 a circumflex */ + { 0x20, 0x56, 0x55, 0x56, 0x78, 0x00 }, + /* 227 a tilde */ + { 0x20, 0x55, 0x56, 0x55, 0x78, 0x00 }, + /* 228 a diaeresis */ + { 0x20, 0x55, 0x54, 0x55, 0x78, 0x00 }, + /* 229 a ring above */ + { 0x20, 0x56, 0x55, 0x56, 0x78, 0x00 }, + + /* 230 ae ligature + * . . . . . . + * . . . . . . + * . # . # # . + * # . # . . # + * # . # # # . + * # . # . . . + * . # . # # . + * . . . . . . + */ + { 0x20, 0x54, 0x78, 0x54, 0x54, 0x18 }, + + /* 231 c cedilla + * c = { 0x38, 0x44, 0x44, 0x44, 0x20 } + * Add cedilla at bottom: col2 gets 0x80 (row 7) + */ + { 0x38, 0x44, 0xC4, 0x44, 0x20, 0x00 }, + + /* 232 e grave + * e = { 0x38, 0x54, 0x54, 0x54, 0x18 } + */ + { 0x38, 0x56, 0x55, 0x54, 0x18, 0x00 }, + /* 233 e acute */ + { 0x38, 0x54, 0x55, 0x56, 0x18, 0x00 }, + /* 234 e circumflex */ + { 0x38, 0x56, 0x55, 0x56, 0x18, 0x00 }, + /* 235 e diaeresis */ + { 0x38, 0x55, 0x54, 0x55, 0x18, 0x00 }, + + /* 236 i grave + * i = { 0x00, 0x44, 0x7D, 0x40, 0x00 } + * Note: 'i' already has a dot (bit 0 of 0x7D = 1). For accented versions + * we replace the dot with the accent mark. + * i without dot: { 0x00, 0x44, 0x7C, 0x40, 0x00 } + */ + { 0x00, 0x46, 0x7D, 0x40, 0x00, 0x00 }, + /* 237 i acute */ + { 0x00, 0x44, 0x7D, 0x42, 0x00, 0x00 }, + /* 238 i circumflex */ + { 0x00, 0x46, 0x7D, 0x42, 0x00, 0x00 }, + /* 239 i diaeresis */ + { 0x00, 0x45, 0x7C, 0x41, 0x00, 0x00 }, + + /* 240 eth (lowercase) + * . . . # . + * . . # . # + * . # # . . + * # . . # . + * # . . # . + * # . . # . + * . # # . . + * . . . . . + */ + { 0x38, 0x44, 0x44, 0x46, 0x39, 0x00 }, + + /* 241 n tilde + * n = { 0x7C, 0x08, 0x04, 0x04, 0x78 } + * Add tilde: col0+=0x01, col1+=0x02, col2+=0x01, col3+=0x02 + */ + { 0x7C, 0x09, 0x06, 0x05, 0x78, 0x00 }, + + /* 242 o grave + * o = { 0x38, 0x44, 0x44, 0x44, 0x38 } + */ + { 0x38, 0x46, 0x45, 0x44, 0x38, 0x00 }, + /* 243 o acute */ + { 0x38, 0x44, 0x45, 0x46, 0x38, 0x00 }, + /* 244 o circumflex */ + { 0x38, 0x46, 0x45, 0x46, 0x38, 0x00 }, + /* 245 o tilde */ + { 0x38, 0x45, 0x46, 0x45, 0x38, 0x00 }, + /* 246 o diaeresis */ + { 0x38, 0x45, 0x44, 0x45, 0x38, 0x00 }, + + /* 247 division sign + * . . . . . + * . . # . . + * . . . . . + * . # # # . + * . . . . . + * . . # . . + * . . . . . + * . . . . . + */ + { 0x00, 0x08, 0x2A, 0x08, 0x00, 0x00 }, + + /* 248 o stroke (o with /) + * . . . . . + * . . . . . + * . # # # . + * # . # . # + * # # . . # + * # . . . # + * . # # # . + * . . . . . + * + * Actually simpler: o with a diagonal slash through it + */ + { 0x38, 0x64, 0x54, 0x4C, 0x38, 0x00 }, + + /* 249 u grave + * u = { 0x3C, 0x40, 0x40, 0x20, 0x7C } + */ + { 0x3C, 0x42, 0x41, 0x20, 0x7C, 0x00 }, + /* 250 u acute */ + { 0x3C, 0x40, 0x41, 0x22, 0x7C, 0x00 }, + /* 251 u circumflex */ + { 0x3C, 0x42, 0x41, 0x22, 0x7C, 0x00 }, + /* 252 u diaeresis */ + { 0x3C, 0x41, 0x40, 0x21, 0x7C, 0x00 }, + + /* 253 y acute + * y = { 0x0C, 0x50, 0x50, 0x50, 0x3C } + * Add acute: col2+=0x01, col3+=0x02 + */ + { 0x0C, 0x50, 0x51, 0x52, 0x3C, 0x00 }, + + /* 254 thorn (lowercase) + * # . . . . + * # . . . . + * # # # . . + * # . . # . + * # . . # . + * # # # . . + * # . . . . + * . . . . . + */ + { 0xFF, 0x24, 0x24, 0x24, 0x18, 0x00 }, + + /* 255 y diaeresis + * y = { 0x0C, 0x50, 0x50, 0x50, 0x3C } + * Add diaeresis: col1+=0x01, col3+=0x01 + */ + { 0x0C, 0x51, 0x50, 0x51, 0x3C, 0x00 }, +}; + +FONT_ENTRY_DEFINE(cfb_font_6x8, + 6, 8, + CFB_FONT_MONO_VPACKED, + cfb_font_0608, + 32, 255); diff --git a/zephcore/helpers/ui/display.c b/zephcore/helpers/ui/display.c new file mode 100644 index 0000000..33de625 --- /dev/null +++ b/zephcore/helpers/ui/display.c @@ -0,0 +1,340 @@ +/* + * ZephCore - Display Abstraction (CFB) + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Wraps Zephyr's Character Framebuffer (CFB) subsystem. + * Auto-detects any Zephyr-supported display from devicetree: + * 1. "zephyr,display" chosen node (standard — works for any display) + * 2. Legacy nodelabels: sh1106, ssd1306 (backwards compat) + * + * Resolution is queried from the driver at runtime — no hardcoded + * dimensions. Layout code should use mc_display_width/height(). + * + * Auto-off timer turns display off after CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS. + */ + +#include "display.h" +#include "doom_game.h" + +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(mc_display, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +/* ========== State ========== */ + +static const struct device *disp_dev; +static bool disp_on; +static bool disp_initialized; + +/* Runtime display geometry (queried from driver) */ +static uint16_t disp_width; +static uint16_t disp_height; +static uint8_t font_w; +static uint8_t font_h; +static bool is_epd; /* true for e-paper displays */ + +/* Auto-off work */ +static struct k_work_delayable auto_off_work; + +static void auto_off_handler(struct k_work *work) +{ + ARG_UNUSED(work); + /* Don't blank display while Doom easter egg is playing */ + if (doom_game_is_running()) { + return; + } + if (disp_on) { + mc_display_off(); + } +} + +/* ========== Early blanking ========== + * OLED controllers (SSD1306, SH1106) turn the display ON during driver init, + * showing stale VRAM from before reset. Our mc_display_init() runs much later + * (after BLE, LoRa, etc.), so there's a visible garbage flash. + * + * Fix: SYS_INIT hook runs right after the driver, sending "Display OFF" before + * main() starts. This is harmless for non-OLED displays (blanking is a no-op + * or already blanked). */ +static int display_early_blank(void) +{ + const struct device *dev = NULL; + + /* Try standard chosen node first */ +#if DT_HAS_CHOSEN(zephyr_display) + dev = DEVICE_DT_GET_OR_NULL(DT_CHOSEN(zephyr_display)); +#endif + /* Legacy nodelabel fallback */ + if (!dev) { + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sh1106)); + } + if (!dev) { + dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ssd1306)); + } + if (dev && device_is_ready(dev)) { + display_blanking_on(dev); + } + return 0; +} +SYS_INIT(display_early_blank, APPLICATION, 99); + +/* ========== Public API ========== */ + +int mc_display_init(void) +{ + /* Find display device from devicetree. + * Priority: zephyr,display chosen > sh1106 nodelabel > ssd1306 nodelabel. + * This supports any Zephyr display driver (SSD1306, SH1106, ST7735, + * ILI9341, SSD1681 e-ink, etc.) via the standard chosen mechanism. */ +#if DT_HAS_CHOSEN(zephyr_display) + disp_dev = DEVICE_DT_GET_OR_NULL(DT_CHOSEN(zephyr_display)); +#endif + if (!disp_dev) { + disp_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sh1106)); + } + if (!disp_dev) { + disp_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ssd1306)); + } + + if (!disp_dev || !device_is_ready(disp_dev)) { + LOG_INF("no display found - display disabled"); + return -ENODEV; + } + + /* Query actual resolution from display driver */ + struct display_capabilities caps; + + display_get_capabilities(disp_dev, &caps); + disp_width = caps.x_resolution; + disp_height = caps.y_resolution; + is_epd = (caps.screen_info & SCREEN_INFO_EPD) != 0; + + LOG_INF("display: %ux%u%s", disp_width, disp_height, + is_epd ? " (e-paper)" : ""); + + /* Blank display before CFB init to hide stale VRAM */ + display_blanking_on(disp_dev); + + /* Initialize CFB */ + int ret = cfb_framebuffer_init(disp_dev); + + if (ret) { + LOG_ERR("CFB init failed: %d", ret); + return ret; + } + + /* Select smallest font — scan all registered fonts and pick the one + * with the smallest height for best text density. Our custom 6x8 + * Latin-1 font will typically win. */ + int num_fonts = cfb_get_numof_fonts(disp_dev); + + LOG_INF("display: %d fonts available", num_fonts); + + int best_idx = 0; + uint8_t best_h = 255; + + for (int i = 0; i < num_fonts; i++) { + uint8_t fw = 0, fh = 0; + + cfb_get_font_size(disp_dev, i, &fw, &fh); + LOG_INF(" font[%d]: %ux%u", i, fw, fh); + if (fh < best_h) { + best_h = fh; + best_idx = i; + } + } + + cfb_framebuffer_set_font(disp_dev, best_idx); + cfb_get_font_size(disp_dev, best_idx, &font_w, &font_h); + LOG_INF("display: selected font[%d] (%ux%u)", best_idx, font_w, font_h); + + /* CFB inversion no longer needed — Zephyr commit 2374ef62f97 fixed + * the MONO10/MONO01 polarity logic in cfb_framebuffer_finalize(). + * SSD1306 OLED reports MONO01 by default, which CFB now handles + * correctly (white pixels on black background) without manual invert. */ + + /* Push a blank frame to clear stale VRAM, then unblank. */ + cfb_framebuffer_clear(disp_dev, false); + cfb_framebuffer_finalize(disp_dev); + + display_blanking_off(disp_dev); + disp_on = true; + disp_initialized = true; + + /* Set up auto-off timer */ + k_work_init_delayable(&auto_off_work, auto_off_handler); + + LOG_INF("display initialized (%ux%u, font %ux%u)", + disp_width, disp_height, font_w, font_h); + return 0; +} + +uint16_t mc_display_width(void) +{ + return disp_width; +} + +uint16_t mc_display_height(void) +{ + return disp_height; +} + +uint8_t mc_display_font_width(void) +{ + return font_w; +} + +uint8_t mc_display_font_height(void) +{ + return font_h; +} + +void mc_display_on(void) +{ + if (!disp_initialized) { + return; + } + + if (!disp_on) { + display_blanking_off(disp_dev); + disp_on = true; + } + + mc_display_reset_auto_off(); +} + +void mc_display_off(void) +{ + if (!disp_initialized) { + return; + } + + if (disp_on) { + display_blanking_on(disp_dev); + disp_on = false; + } +} + +bool mc_display_is_on(void) +{ + return disp_on; +} + +void mc_display_clear(void) +{ + if (!disp_initialized) { + return; + } + + cfb_framebuffer_clear(disp_dev, false); +} + +void mc_display_text(int x, int y, const char *text, bool invert) +{ + if (!disp_initialized || !text) { + return; + } + + if (invert) { + cfb_framebuffer_invert(disp_dev); + } + + cfb_print(disp_dev, text, x, y); + + if (invert) { + cfb_framebuffer_invert(disp_dev); + } +} + +void mc_display_fill_rect(int x, int y, int w, int h) +{ + if (!disp_initialized) { + return; + } + + /* CFB doesn't have a native fill_rect, so we draw line by line */ + for (int row = y; row < y + h && row < disp_height; row++) { + struct cfb_position start = { .x = x, .y = row }; + struct cfb_position end = { .x = x + w - 1, .y = row }; + cfb_draw_line(disp_dev, &start, &end); + } +} + +void mc_display_hline(int x, int y, int w) +{ + if (!disp_initialized) { + return; + } + + struct cfb_position start = { .x = x, .y = y }; + struct cfb_position end = { .x = x + w - 1, .y = y }; + cfb_draw_line(disp_dev, &start, &end); +} + +void mc_display_xbm(int x, int y, const uint8_t *data, int w, int h) +{ + if (!disp_initialized || !data) { + return; + } + + /* Adafruit drawBitmap format (MSB first): row-major, bit 7 = leftmost. + * This matches the Arduino MeshCore logo data from icons.h. + * Each row is padded to byte boundary: bytes_per_row = (w+7)/8 */ + int bytes_per_row = (w + 7) / 8; + + for (int row = 0; row < h; row++) { + for (int col = 0; col < w; col++) { + int byte_idx = row * bytes_per_row + col / 8; + int bit_idx = 7 - (col % 8); /* MSB first */ + + if (data[byte_idx] & (1 << bit_idx)) { + struct cfb_position pos = { + .x = (int16_t)(x + col), + .y = (int16_t)(y + row) + }; + cfb_draw_point(disp_dev, &pos); + } + } + } +} + +void mc_display_finalize(void) +{ + if (!disp_initialized) { + return; + } + + /* Don't let CFB overwrite display while Doom is rendering directly */ + if (doom_game_is_running()) { + return; + } + + cfb_framebuffer_finalize(disp_dev); +} + +void mc_display_reset_auto_off(void) +{ + if (!disp_initialized) { + return; + } + +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS + uint32_t timeout = CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS; + + if (timeout > 0) { + k_work_reschedule(&auto_off_work, K_MSEC(timeout)); + } +#endif +} + +const struct device *mc_display_get_device(void) +{ + return disp_initialized ? disp_dev : NULL; +} diff --git a/zephcore/helpers/ui/display.h b/zephcore/helpers/ui/display.h new file mode 100644 index 0000000..4bc3551 --- /dev/null +++ b/zephcore/helpers/ui/display.h @@ -0,0 +1,144 @@ +/* + * ZephCore - Display Abstraction (CFB) + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Wraps Zephyr's Character Framebuffer (CFB) subsystem with: + * - Auto-detection from devicetree (any Zephyr-supported display) + * - Runtime resolution query (supports any size, not just 128x64) + * - Auto-off timer via k_work_delayable + * - Simple text/rect drawing API for UI pages + * + * All functions prefixed mc_display_ to avoid collision with + * Zephyr's display_* namespace in . + */ + +#ifndef ZEPHCORE_DISPLAY_H +#define ZEPHCORE_DISPLAY_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the display from devicetree. + * Detects any Zephyr-supported display via: + * 1. "zephyr,display" chosen node (standard) + * 2. Legacy nodelabels: sh1106, ssd1306 (backwards compat) + * + * Queries actual resolution from driver — no hardcoded dimensions. + * + * @return 0 on success, negative errno on failure, -ENODEV if no display + */ +int mc_display_init(void); + +/** + * Get display width in pixels (queried from hardware at init). + * Returns 0 if display not initialized. + */ +uint16_t mc_display_width(void); + +/** + * Get display height in pixels (queried from hardware at init). + * Returns 0 if display not initialized. + */ +uint16_t mc_display_height(void); + +/** + * Get active font width in pixels. + * Returns 0 if display not initialized. + */ +uint8_t mc_display_font_width(void); + +/** + * Get active font height in pixels. + * Returns 0 if display not initialized. + */ +uint8_t mc_display_font_height(void); + +/** + * Turn display on (wake from blanking). + * Resets the auto-off timer. + */ +void mc_display_on(void); + +/** + * Turn display off (blanking). + */ +void mc_display_off(void); + +/** + * @return true if the display is currently on + */ +bool mc_display_is_on(void); + +/** + * Clear the framebuffer (fill with black). + * Call before rendering a new frame. + */ +void mc_display_clear(void); + +/** + * Draw text at position. + * + * @param x X position in pixels + * @param y Y position in pixels + * @param text Null-terminated string + * @param invert If true, draw black text on white background + */ +void mc_display_text(int x, int y, const char *text, bool invert); + +/** + * Draw a filled rectangle. + * + * @param x Top-left X + * @param y Top-left Y + * @param w Width + * @param h Height + */ +void mc_display_fill_rect(int x, int y, int w, int h); + +/** + * Draw a horizontal line. + */ +void mc_display_hline(int x, int y, int w); + +/** + * Draw a monochrome bitmap (Adafruit/Arduino format). + * MSB first, row-major, 1=foreground. + * Compatible with Arduino's drawBitmap() and MeshCore icons.h data. + * + * @param x Top-left X position + * @param y Top-left Y position + * @param data Bitmap data (MSB first, row-major) + * @param w Width in pixels + * @param h Height in pixels + */ +void mc_display_xbm(int x, int y, const uint8_t *data, int w, int h); + +/** + * Flush the framebuffer to the display hardware. + * Call after all drawing operations for a frame are complete. + */ +void mc_display_finalize(void); + +/** + * Reset the auto-off timer (called on user interaction). + */ +void mc_display_reset_auto_off(void); + +/** + * Get the raw display device pointer. + * Used by easter egg (Doom) to bypass CFB and write directly. + * Returns NULL if display not initialized. + */ +const struct device *mc_display_get_device(void); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHCORE_DISPLAY_H */ diff --git a/zephcore/helpers/ui/doom_game.c b/zephcore/helpers/ui/doom_game.c new file mode 100644 index 0000000..f127bf2 --- /dev/null +++ b/zephcore/helpers/ui/doom_game.c @@ -0,0 +1,1208 @@ +/* + * ZephCore - Doom Raycaster Easter Egg + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Wolf3D-style raycasting engine merged into a single file. + * Runs on k_work_delayable at 20 FPS on the system work queue. + * Writes directly to display via display_write() (bypasses CFB). + * + * ~1.7KB RAM, ~5KB flash when enabled. + */ + +#include "doom_game.h" +#include "display.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef CONFIG_ZEPHCORE_UI_BUZZER +#include "buzzer.h" +#endif + +#include +LOG_MODULE_REGISTER(doom_game, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +/* ========== CONSTANTS ========== */ + +#define SCREEN_W 128 +#define SCREEN_H 64 +#define MAP_W 16 +#define MAP_H 16 +#define FB_SIZE (SCREEN_W * (SCREEN_H / 8)) + +#define FP_SHIFT 16 +#define FP_ONE (1 << FP_SHIFT) +#define FP_HALF (FP_ONE >> 1) + +#define TARGET_FPS 20 +#define FRAME_MS (1000 / TARGET_FPS) +#define MOVE_SPEED (FP_ONE / 8) +#define ROT_SPEED (FP_ONE / 12) +#define COLLISION_R (FP_ONE / 4) +#define MAX_ENEMIES 8 + +/* Input bitflags */ +#define DINPUT_FWD BIT(0) +#define DINPUT_BACK BIT(1) +#define DINPUT_LEFT BIT(2) +#define DINPUT_RIGHT BIT(3) +#define DINPUT_FIRE BIT(4) + +/* ========== FIXED-POINT MATH ========== */ + +typedef int32_t fixed_t; + +static inline fixed_t fp_mul(fixed_t a, fixed_t b) +{ + return (fixed_t)(((int64_t)a * b) >> FP_SHIFT); +} + +static inline fixed_t fp_div(fixed_t a, fixed_t b) +{ + if (b == 0) return a > 0 ? INT32_MAX : INT32_MIN; + return (fixed_t)(((int64_t)a << FP_SHIFT) / b); +} + +static inline fixed_t fp_from_int(int x) +{ + return x << FP_SHIFT; +} + +static inline int fp_to_int(fixed_t x) +{ + return x >> FP_SHIFT; +} + +static inline fixed_t fp_abs(fixed_t x) +{ + return x < 0 ? -x : x; +} + +/* ========== GAME TYPES ========== */ + +enum enemy_type { + ENEMY_NONE = 0, + ENEMY_IMP, + ENEMY_DEMON, +}; + +enum enemy_state { + ESTATE_IDLE, + ESTATE_CHASE, + ESTATE_ATTACK, + ESTATE_DYING, + ESTATE_DEAD, +}; + +struct doom_player { + fixed_t x, y; + fixed_t dir_x, dir_y; + fixed_t plane_x, plane_y; + int health; + int ammo; + bool firing; +}; + +struct doom_enemy { + enum enemy_type type; + enum enemy_state state; + fixed_t x, y; + int health; + int anim_tick; +}; + +struct doom_state { + struct doom_player player; + struct doom_enemy enemies[MAX_ENEMIES]; + int num_enemies; + int level; + int score; + bool game_over; + uint32_t frame_count; +}; + +/* ========== STATIC STATE ========== */ + +static bool running; +static struct doom_state game; +static uint8_t render_fb[FB_SIZE]; +static atomic_t input_state; +static fixed_t zbuffer[SCREEN_W]; +static int fire_cooldown; + +/* Game tick work */ +static struct k_work_delayable game_tick_work; + +/* ========== SOUND ========== */ + +#if DT_HAS_ALIAS(buzzer) +static const struct pwm_dt_spec doom_buzzer = PWM_DT_SPEC_GET(DT_ALIAS(buzzer)); +#define HAS_DOOM_BUZZER 1 +#else +#define HAS_DOOM_BUZZER 0 +#endif + +struct tone { + uint16_t freq; + uint16_t dur_ms; +}; + +static const struct tone snd_shoot[] = { + {800, 30}, {400, 30}, {200, 40}, {0, 0} +}; +static const struct tone snd_hit[] = { + {300, 50}, {150, 80}, {0, 0} +}; +static const struct tone snd_death[] = { + {500, 80}, {400, 80}, {300, 100}, {200, 120}, {100, 150}, {0, 0} +}; + +static const struct tone *current_sound; +static struct k_work_delayable sound_work; + +static bool sound_allowed(void) +{ +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + return !buzzer_is_quiet(); +#else + return true; +#endif +} + +static void doom_sound_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + +#if HAS_DOOM_BUZZER + if (!current_sound || current_sound->freq == 0) { + pwm_set_pulse_dt(&doom_buzzer, 0); + current_sound = NULL; + return; + } + + uint32_t period = 1000000000U / current_sound->freq; + pwm_set_dt(&doom_buzzer, period, period / 2); + + uint16_t dur = current_sound->dur_ms; + current_sound++; + + k_work_reschedule(&sound_work, K_MSEC(dur)); +#endif +} + +static void doom_play_sound(const struct tone *seq) +{ +#if HAS_DOOM_BUZZER + if (!sound_allowed()) { + return; + } + current_sound = seq; + k_work_reschedule(&sound_work, K_NO_WAIT); +#else + ARG_UNUSED(seq); +#endif +} + +static void doom_sound_off(void) +{ +#if HAS_DOOM_BUZZER + k_work_cancel_delayable(&sound_work); + if (pwm_is_ready_dt(&doom_buzzer)) { + pwm_set_pulse_dt(&doom_buzzer, 0); + } + current_sound = NULL; +#endif +} + +/* ========== MAP ========== */ + +static const uint8_t level_1[MAP_H][MAP_W] = { + {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,2,2,2,0,0,0,0,3,3,3,0,0,1}, + {1,0,0,2,0,0,0,0,0,0,0,0,3,0,0,1}, + {1,0,0,2,0,0,0,0,0,0,0,0,3,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,4,4,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,4,4,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,3,0,0,0,0,0,0,0,0,2,0,0,1}, + {1,0,0,3,0,0,0,0,0,0,0,0,2,0,0,1}, + {1,0,0,3,3,3,0,0,0,0,2,2,2,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, + {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, +}; + +static const uint8_t *current_map; + +static void doom_map_init(void) +{ + current_map = &level_1[0][0]; +} + +static uint8_t doom_map_get(int x, int y) +{ + if (x < 0 || x >= MAP_W || y < 0 || y >= MAP_H) { + return 1; + } + return current_map[y * MAP_W + x]; +} + +/* ========== WALL TEXTURES (8x8 1-bit) ========== */ + +static const uint8_t tex_brick[8] = { + 0xFF, 0x88, 0xFF, 0xFF, 0x22, 0xFF, 0xFF, 0x88, +}; +static const uint8_t tex_stone[8] = { + 0xEE, 0xBB, 0xFF, 0x77, 0xFF, 0xDD, 0xBB, 0xFF, +}; +static const uint8_t tex_metal[8] = { + 0xFF, 0x81, 0xA5, 0x81, 0x81, 0xA5, 0x81, 0xFF, +}; +static const uint8_t tex_pillar[8] = { + 0xB6, 0xB6, 0xB6, 0xFF, 0x6D, 0x6D, 0x6D, 0xFF, +}; + +static const uint8_t *wall_textures[5] = { + tex_brick, tex_brick, tex_stone, tex_metal, tex_pillar, +}; + +/* ========== BAYER 4x4 DITHERING ========== */ + +static const uint8_t bayer4x4[4][4] = { + { 0, 8, 2, 10}, + {12, 4, 14, 6}, + { 3, 11, 1, 9}, + {15, 7, 13, 5}, +}; + +static int dist_to_brightness(fixed_t dist) +{ + int d = fp_to_int(dist); + if (d < 1) return 15; + if (d >= 8) return 1; + return 15 - (d * 2); +} + +static bool textured_pixel(int screen_x, int screen_y, + int tex_x, int tex_y, + uint8_t wall_type, bool side_hit, + fixed_t perp_dist) +{ + const uint8_t *tex = wall_textures[wall_type < 5 ? wall_type : 0]; + bool tex_on = (tex[tex_y & 7] & (0x80 >> (tex_x & 7))) != 0; + + if (!tex_on) return false; + + int brightness = dist_to_brightness(perp_dist); + if (side_hit) { + brightness = (brightness * 3) / 4; + } + + uint8_t threshold = bayer4x4[screen_y & 3][screen_x & 3]; + return brightness > threshold; +} + +static bool floor_pixel(int x, int y) +{ + int dy = y - (SCREEN_H / 2); + if (dy <= 0) return false; + + int brightness; + if (dy < 4) brightness = 6; + else if (dy < 8) brightness = 4; + else if (dy < 16) brightness = 3; + else brightness = 2; + + uint8_t threshold = bayer4x4[y & 3][x & 3]; + return brightness > threshold; +} + +static bool ceiling_pixel(int x, int y) +{ + int dy = (SCREEN_H / 2) - y; + if (dy <= 0) return false; + + int brightness; + if (dy < 4) brightness = 3; + else if (dy < 8) brightness = 2; + else brightness = 1; + + uint8_t threshold = bayer4x4[y & 3][x & 3]; + return brightness > threshold; +} + +/* ========== ENEMY SPRITES (16x16 1-bit) ========== */ + +static const uint16_t spr_imp_idle[16] = { + 0x07C0, 0x0FE0, 0x1BB0, 0x1FF0, 0x0FE0, 0x07C0, 0x27C8, 0x77DC, + 0xFFFE, 0xEFEE, 0x47C4, 0x07C0, 0x06C0, 0x0EE0, 0x0C60, 0x1C70, +}; +static const uint16_t spr_imp_attack[16] = { + 0x07C0, 0x0FE0, 0x1BB0, 0x1FF0, 0x0FE0, 0x07C0, 0x07C0, 0xFFFE, + 0xFFFE, 0xE7CE, 0x07C0, 0x07C0, 0x06C0, 0x0EE0, 0x0C60, 0x1C70, +}; +static const uint16_t spr_demon_idle[16] = { + 0x4004, 0x600C, 0x783C, 0x7FFC, 0xFFFE, 0xFBBE, 0xFFFE, 0x7FFC, + 0x7FFC, 0x7FFC, 0xFFFE, 0xFFFE, 0xF83E, 0xE82E, 0xC826, 0xC826, +}; +static const uint16_t spr_demon_attack[16] = { + 0x4004, 0x600C, 0x783C, 0x7FFC, 0xFFFE, 0xF03E, 0xFBBE, 0x787C, + 0x7FFC, 0x7FFC, 0xFFFE, 0xFFFE, 0xF83E, 0xE82E, 0xC826, 0xC826, +}; +static const uint16_t spr_dying[16] = { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0200, 0x1290, 0x0840, 0x27C8, + 0x1FF0, 0x3FF8, 0x7FFC, 0x7FFC, 0xFFFE, 0xFFFE, 0xFFFE, 0xFFFE, +}; +static const uint16_t spr_dead[16] = { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x4488, 0x2B54, 0x7FFC, 0xFFFE, +}; + +static const uint16_t *get_sprite_16(enum enemy_type type, + enum enemy_state state, + int anim_tick) +{ + if (state == ESTATE_DEAD) return spr_dead; + if (state == ESTATE_DYING) return spr_dying; + + switch (type) { + case ENEMY_IMP: + return (state == ESTATE_ATTACK && (anim_tick & 4)) + ? spr_imp_attack : spr_imp_idle; + case ENEMY_DEMON: + return (state == ESTATE_ATTACK && (anim_tick & 4)) + ? spr_demon_attack : spr_demon_idle; + default: + return spr_imp_idle; + } +} + +/* ========== RAYCASTER ========== */ + +static void render_sprites(void) +{ + struct doom_player *p = &game.player; + + struct sprite_order { + int idx; + fixed_t dist; + } order[MAX_ENEMIES]; + int visible = 0; + + for (int i = 0; i < game.num_enemies; i++) { + struct doom_enemy *e = &game.enemies[i]; + if (e->type == ENEMY_NONE || e->state == ESTATE_DEAD) continue; + + fixed_t dx = e->x - p->x; + fixed_t dy = e->y - p->y; + order[visible].idx = i; + order[visible].dist = fp_mul(dx, dx) + fp_mul(dy, dy); + visible++; + } + + if (visible == 0) return; + + /* Insertion sort — farthest first */ + for (int i = 1; i < visible; i++) { + struct sprite_order tmp = order[i]; + int j = i - 1; + while (j >= 0 && order[j].dist < tmp.dist) { + order[j + 1] = order[j]; + j--; + } + order[j + 1] = tmp; + } + + fixed_t det = fp_mul(p->plane_x, p->dir_y) - + fp_mul(p->dir_x, p->plane_y); + if (det == 0) return; + fixed_t inv_det = fp_div(FP_ONE, det); + + for (int i = 0; i < visible; i++) { + struct doom_enemy *e = &game.enemies[order[i].idx]; + const uint16_t *sprite = get_sprite_16(e->type, e->state, + e->anim_tick); + + fixed_t sx = e->x - p->x; + fixed_t sy = e->y - p->y; + + fixed_t transform_x = fp_mul(inv_det, + fp_mul(p->dir_y, sx) - fp_mul(p->dir_x, sy)); + fixed_t transform_y = fp_mul(inv_det, + fp_mul(-p->plane_y, sx) + fp_mul(p->plane_x, sy)); + + if (transform_y <= 0) continue; + + int sprite_screen_x = (SCREEN_W / 2) + + fp_to_int(fp_div(fp_mul(transform_x, + fp_from_int(SCREEN_W)), transform_y)); + + int sprite_h = fp_to_int(fp_abs(fp_div( + fp_from_int(SCREEN_H), transform_y))); + if (sprite_h < 3) continue; + if (sprite_h > SCREEN_H * 2) sprite_h = SCREEN_H * 2; + + int draw_start_y = (SCREEN_H - sprite_h) / 2; + int draw_end_y = draw_start_y + sprite_h; + int draw_start_x = sprite_screen_x - sprite_h / 2; + int draw_end_x = sprite_screen_x + sprite_h / 2; + + int brightness = dist_to_brightness(transform_y); + + for (int sc = draw_start_x; sc < draw_end_x; sc++) { + if (sc < 0 || sc >= SCREEN_W) continue; + if (transform_y >= zbuffer[sc]) continue; + + int tex_x = (sc - draw_start_x) * 16 / sprite_h; + if (tex_x < 0) tex_x = 0; + if (tex_x > 15) tex_x = 15; + + for (int sr = draw_start_y; sr < draw_end_y; sr++) { + if (sr < 0 || sr >= SCREEN_H) continue; + + int tex_y = (sr - draw_start_y) * 16 / sprite_h; + if (tex_y < 0) tex_y = 0; + if (tex_y > 15) tex_y = 15; + + if (sprite[tex_y] & (0x8000 >> tex_x)) { + uint8_t threshold = + bayer4x4[sr & 3][sc & 3]; + if (brightness > threshold) { + render_fb[(sr / 8) * SCREEN_W + sc] |= + (1U << (sr & 7)); + } + } + } + } + } +} + +static void raycaster_render(void) +{ + struct doom_player *p = &game.player; + + for (int x = 0; x < SCREEN_W; x++) { + fixed_t camera_x = fp_div(fp_from_int(2 * x - SCREEN_W), + fp_from_int(SCREEN_W)); + + fixed_t ray_dir_x = p->dir_x + fp_mul(p->plane_x, camera_x); + fixed_t ray_dir_y = p->dir_y + fp_mul(p->plane_y, camera_x); + + int map_x = fp_to_int(p->x); + int map_y = fp_to_int(p->y); + + fixed_t delta_dist_x = (ray_dir_x == 0) + ? INT32_MAX + : fp_abs(fp_div(FP_ONE, ray_dir_x)); + fixed_t delta_dist_y = (ray_dir_y == 0) + ? INT32_MAX + : fp_abs(fp_div(FP_ONE, ray_dir_y)); + + int step_x, step_y; + fixed_t side_dist_x, side_dist_y; + + if (ray_dir_x < 0) { + step_x = -1; + side_dist_x = fp_mul(p->x - fp_from_int(map_x), + delta_dist_x); + } else { + step_x = 1; + side_dist_x = fp_mul(fp_from_int(map_x + 1) - p->x, + delta_dist_x); + } + + if (ray_dir_y < 0) { + step_y = -1; + side_dist_y = fp_mul(p->y - fp_from_int(map_y), + delta_dist_y); + } else { + step_y = 1; + side_dist_y = fp_mul(fp_from_int(map_y + 1) - p->y, + delta_dist_y); + } + + bool hit = false; + bool side = false; + uint8_t wall_type = 0; + + for (int i = 0; i < 64; i++) { + if (side_dist_x < side_dist_y) { + side_dist_x += delta_dist_x; + map_x += step_x; + side = false; + } else { + side_dist_y += delta_dist_y; + map_y += step_y; + side = true; + } + + wall_type = doom_map_get(map_x, map_y); + if (wall_type > 0) { + hit = true; + break; + } + } + + fixed_t perp_dist; + if (!side) { + perp_dist = side_dist_x - delta_dist_x; + } else { + perp_dist = side_dist_y - delta_dist_y; + } + + if (perp_dist < (FP_ONE / 16)) { + perp_dist = FP_ONE / 16; + } + + zbuffer[x] = perp_dist; + + if (!hit) { + for (int y = 0; y < SCREEN_H / 2; y++) { + if (ceiling_pixel(x, y)) { + render_fb[(y / 8) * SCREEN_W + x] |= + (1U << (y & 7)); + } + } + for (int y = SCREEN_H / 2; y < SCREEN_H; y++) { + if (floor_pixel(x, y)) { + render_fb[(y / 8) * SCREEN_W + x] |= + (1U << (y & 7)); + } + } + continue; + } + + int line_height = fp_to_int(fp_div(fp_from_int(SCREEN_H), + perp_dist)); + + int draw_start = (SCREEN_H - line_height) / 2; + int draw_end = draw_start + line_height; + + fixed_t wall_x; + if (!side) { + wall_x = p->y + fp_mul(perp_dist, ray_dir_y); + } else { + wall_x = p->x + fp_mul(perp_dist, ray_dir_x); + } + wall_x -= fp_from_int(fp_to_int(wall_x)); + + int tex_x = fp_to_int(fp_mul(wall_x, fp_from_int(8))); + if (tex_x < 0) tex_x = 0; + if (tex_x > 7) tex_x = 7; + + if ((!side && ray_dir_x > 0) || (side && ray_dir_y < 0)) { + tex_x = 7 - tex_x; + } + + int actual_start = draw_start < 0 ? 0 : draw_start; + int actual_end = draw_end >= SCREEN_H ? SCREEN_H - 1 : draw_end; + + for (int y = actual_start; y <= actual_end; y++) { + int tex_y = ((y - draw_start) * 8) / line_height; + if (tex_y < 0) tex_y = 0; + if (tex_y > 7) tex_y = 7; + + if (textured_pixel(x, y, tex_x, tex_y, + wall_type, side, perp_dist)) { + render_fb[(y / 8) * SCREEN_W + x] |= + (1U << (y & 7)); + } + } + + for (int y = 0; y < actual_start; y++) { + if (ceiling_pixel(x, y)) { + render_fb[(y / 8) * SCREEN_W + x] |= + (1U << (y & 7)); + } + } + + for (int y = actual_end + 1; y < SCREEN_H; y++) { + if (floor_pixel(x, y)) { + render_fb[(y / 8) * SCREEN_W + x] |= + (1U << (y & 7)); + } + } + } + + render_sprites(); +} + +/* ========== PIXEL HELPERS ========== */ + +static inline void set_px(int x, int y) +{ + if (x >= 0 && x < SCREEN_W && y >= 0 && y < SCREEN_H) { + render_fb[(y / 8) * SCREEN_W + x] |= (1U << (y & 7)); + } +} + +/* ========== 4x5 DIGIT FONT ========== */ + +static const uint8_t digits[10][5] = { + {0xF, 0x9, 0x9, 0x9, 0xF}, + {0x2, 0x6, 0x2, 0x2, 0x7}, + {0xF, 0x1, 0xF, 0x8, 0xF}, + {0xF, 0x1, 0xF, 0x1, 0xF}, + {0x9, 0x9, 0xF, 0x1, 0x1}, + {0xF, 0x8, 0xF, 0x1, 0xF}, + {0xF, 0x8, 0xF, 0x9, 0xF}, + {0xF, 0x1, 0x2, 0x4, 0x4}, + {0xF, 0x9, 0xF, 0x9, 0xF}, + {0xF, 0x9, 0xF, 0x1, 0xF}, +}; + +static const uint8_t glyph_H[5] = {0x9, 0x9, 0xF, 0x9, 0x9}; +static const uint8_t glyph_P[5] = {0xE, 0x9, 0xE, 0x8, 0x8}; +static const uint8_t glyph_A[5] = {0x6, 0x9, 0xF, 0x9, 0x9}; +static const uint8_t glyph_M[5] = {0x9, 0xF, 0xF, 0x9, 0x9}; + +static void draw_glyph(int dx, int dy, const uint8_t *glyph) +{ + for (int row = 0; row < 5; row++) { + for (int col = 0; col < 4; col++) { + if (glyph[row] & (8 >> col)) { + set_px(dx + col, dy + row); + } + } + } +} + +static void draw_digit(int dx, int dy, int d) +{ + if (d < 0 || d > 9) return; + draw_glyph(dx, dy, digits[d]); +} + +static void draw_number(int x, int y, int val) +{ + if (val < 0) val = 0; + if (val > 999) val = 999; + if (val >= 100) { + draw_digit(x, y, val / 100); + x += 5; + } + if (val >= 10) { + draw_digit(x, y, (val / 10) % 10); + x += 5; + } + draw_digit(x, y, val % 10); +} + +/* ========== HUD ========== */ + +static void draw_health_bar(int x, int y, int hp) +{ + int bar_w = 20; + int filled = (hp * bar_w) / 100; + if (filled < 0) filled = 0; + if (filled > bar_w) filled = bar_w; + + for (int bx = x - 1; bx <= x + bar_w; bx++) { + set_px(bx, y - 1); + set_px(bx, y + 3); + } + for (int by = y; by < y + 3; by++) { + set_px(x - 1, by); + set_px(x + bar_w, by); + } + + for (int bx = x; bx < x + filled; bx++) { + for (int by = y; by < y + 3; by++) { + set_px(bx, by); + } + } +} + +static void draw_hud(void) +{ + memset(&render_fb[7 * SCREEN_W], 0, SCREEN_W); + + for (int x = 0; x < SCREEN_W; x++) { + render_fb[7 * SCREEN_W + x] |= 0x01; + } + + draw_glyph(1, 58, glyph_H); + draw_glyph(6, 58, glyph_P); + draw_health_bar(12, 58, game.player.health); + + draw_glyph(48, 58, glyph_A); + draw_glyph(53, 58, glyph_M); + draw_number(59, 58, game.player.ammo); + + draw_number(100, 58, game.score); +} + +/* ========== GUN SPRITE ========== */ + +static const uint16_t gun_idle[12] = { + 0x0180, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x0FF0, + 0x1FF8, 0x1FF8, 0x3FFC, 0x398C, 0x7186, 0x6186, +}; +static const uint16_t gun_fire[12] = { + 0x0990, 0x1248, 0x07E0, 0x0FF0, 0x0FF0, 0x1FF8, + 0x3FFC, 0x3FFC, 0x7FFE, 0x798E, 0xF187, 0xE187, +}; + +static void draw_gun(bool firing) +{ + const uint16_t *sprite = firing ? gun_fire : gun_idle; + int cx = SCREEN_W / 2 - 8; + int by = SCREEN_H - 12 - 8; + + if (firing) by -= 1; + + for (int row = 0; row < 12; row++) { + for (int col = 0; col < 16; col++) { + if (sprite[row] & (0x8000 >> col)) { + set_px(cx + col, by + row); + } + } + } + + int ch_x = SCREEN_W / 2; + int ch_y = SCREEN_H / 2 - 4; + set_px(ch_x, ch_y - 2); + set_px(ch_x, ch_y - 1); + set_px(ch_x, ch_y + 1); + set_px(ch_x, ch_y + 2); + set_px(ch_x - 2, ch_y); + set_px(ch_x - 1, ch_y); + set_px(ch_x + 1, ch_y); + set_px(ch_x + 2, ch_y); +} + +/* ========== GAME OVER SCREEN ========== */ + +static const uint8_t letter_D[8] = { + 0xFC, 0xFE, 0x86, 0x86, 0x86, 0x86, 0xFE, 0xFC +}; +static const uint8_t letter_E[8] = { + 0xFE, 0xFE, 0x80, 0xF8, 0xF8, 0x80, 0xFE, 0xFE +}; +static const uint8_t letter_A[8] = { + 0x7C, 0xFE, 0x86, 0x86, 0xFE, 0xFE, 0x86, 0x86 +}; + +static void blit_letter(int lx, int ly, const uint8_t *data) +{ + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 8; col++) { + if (data[row] & (0x80 >> col)) { + int px = lx + col; + int py = ly + row; + if (px >= 0 && px < SCREEN_W && + py >= 0 && py < SCREEN_H) { + render_fb[(py/8)*SCREEN_W + px] |= + (1U << (py & 7)); + } + } + } + } +} + +static void draw_game_over(void) +{ + memset(render_fb, 0, FB_SIZE); + + int start_x = (SCREEN_W - 38) / 2; + int start_y = 20; + + blit_letter(start_x, start_y, letter_D); + blit_letter(start_x + 10, start_y, letter_E); + blit_letter(start_x + 20, start_y, letter_A); + blit_letter(start_x + 30, start_y, letter_D); + + draw_number((SCREEN_W - 12) / 2, start_y + 14, game.score); +} + +/* ========== DISPLAY ========== */ + +static void doom_flush_fb(void) +{ + const struct device *dev = mc_display_get_device(); + if (!dev) return; + + struct display_buffer_descriptor desc = { + .buf_size = FB_SIZE, + .width = SCREEN_W, + .height = SCREEN_H, + .pitch = SCREEN_W, + }; + + display_write(dev, 0, 0, &desc, render_fb); +} + +/* ========== GAME LOGIC ========== */ + +static void game_init(void) +{ + memset(&game, 0, sizeof(game)); + + game.player.x = fp_from_int(8) + FP_HALF; + game.player.y = fp_from_int(13) + FP_HALF; + game.player.health = 100; + game.player.ammo = 50; + game.player.dir_x = 0; + game.player.dir_y = -FP_ONE; + game.player.plane_x = (FP_ONE * 2) / 3; + game.player.plane_y = 0; + + game.enemies[0] = (struct doom_enemy){ + .type = ENEMY_IMP, .state = ESTATE_IDLE, + .x = fp_from_int(4) + FP_HALF, + .y = fp_from_int(4) + FP_HALF, + .health = 30, + }; + game.enemies[1] = (struct doom_enemy){ + .type = ENEMY_DEMON, .state = ESTATE_IDLE, + .x = fp_from_int(12) + FP_HALF, + .y = fp_from_int(4) + FP_HALF, + .health = 60, + }; + game.enemies[2] = (struct doom_enemy){ + .type = ENEMY_IMP, .state = ESTATE_IDLE, + .x = fp_from_int(8) + FP_HALF, + .y = fp_from_int(8) + FP_HALF, + .health = 30, + }; + game.enemies[3] = (struct doom_enemy){ + .type = ENEMY_DEMON, .state = ESTATE_IDLE, + .x = fp_from_int(4) + FP_HALF, + .y = fp_from_int(10) + FP_HALF, + .health = 60, + }; + game.num_enemies = 4; + game.level = 1; + game.score = 0; + game.game_over = false; + + fire_cooldown = 0; + atomic_set(&input_state, 0); + doom_map_init(); +} + +static bool can_move(fixed_t new_x, fixed_t new_y) +{ + int checks[][2] = { + {fp_to_int(new_x + COLLISION_R), fp_to_int(new_y + COLLISION_R)}, + {fp_to_int(new_x - COLLISION_R), fp_to_int(new_y + COLLISION_R)}, + {fp_to_int(new_x + COLLISION_R), fp_to_int(new_y - COLLISION_R)}, + {fp_to_int(new_x - COLLISION_R), fp_to_int(new_y - COLLISION_R)}, + }; + + for (int i = 0; i < 4; i++) { + if (doom_map_get(checks[i][0], checks[i][1]) > 0) { + return false; + } + } + return true; +} + +static void handle_movement(uint32_t input) +{ + struct doom_player *p = &game.player; + + if (input & DINPUT_FWD) { + fixed_t nx = p->x + fp_mul(p->dir_x, MOVE_SPEED); + fixed_t ny = p->y + fp_mul(p->dir_y, MOVE_SPEED); + if (can_move(nx, p->y)) p->x = nx; + if (can_move(p->x, ny)) p->y = ny; + } + + if (input & DINPUT_BACK) { + fixed_t nx = p->x - fp_mul(p->dir_x, MOVE_SPEED); + fixed_t ny = p->y - fp_mul(p->dir_y, MOVE_SPEED); + if (can_move(nx, p->y)) p->x = nx; + if (can_move(p->x, ny)) p->y = ny; + } + + if (input & DINPUT_LEFT) { + fixed_t old_dx = p->dir_x; + fixed_t old_px = p->plane_x; + fixed_t cos_r = FP_ONE - (ROT_SPEED / 8); + fixed_t sin_r = ROT_SPEED; + + p->dir_x = fp_mul(old_dx, cos_r) - fp_mul(p->dir_y, sin_r); + p->dir_y = fp_mul(old_dx, sin_r) + fp_mul(p->dir_y, cos_r); + p->plane_x = fp_mul(old_px, cos_r) - fp_mul(p->plane_y, sin_r); + p->plane_y = fp_mul(old_px, sin_r) + fp_mul(p->plane_y, cos_r); + } + + if (input & DINPUT_RIGHT) { + fixed_t old_dx = p->dir_x; + fixed_t old_px = p->plane_x; + fixed_t cos_r = FP_ONE - (ROT_SPEED / 8); + fixed_t sin_r = ROT_SPEED; + + p->dir_x = fp_mul(old_dx, cos_r) + fp_mul(p->dir_y, sin_r); + p->dir_y = -fp_mul(old_dx, sin_r) + fp_mul(p->dir_y, cos_r); + p->plane_x = fp_mul(old_px, cos_r) + fp_mul(p->plane_y, sin_r); + p->plane_y = -fp_mul(old_px, sin_r) + fp_mul(p->plane_y, cos_r); + } +} + +static void handle_shooting(uint32_t input) +{ + struct doom_player *p = &game.player; + + if (!(input & DINPUT_FIRE) || p->ammo <= 0) { + p->firing = false; + return; + } + + if (p->firing) return; + + p->firing = true; + p->ammo--; + doom_play_sound(snd_shoot); + + for (int i = 0; i < game.num_enemies; i++) { + struct doom_enemy *e = &game.enemies[i]; + if (e->type == ENEMY_NONE || e->state >= ESTATE_DYING) continue; + + fixed_t dx = e->x - p->x; + fixed_t dy = e->y - p->y; + + fixed_t dist = fp_mul(dx, dx) + fp_mul(dy, dy); + if (dist > fp_from_int(10 * 10)) continue; + + fixed_t dot = fp_mul(dx, p->dir_x) + fp_mul(dy, p->dir_y); + if (dot <= 0) continue; + + fixed_t cross = fp_abs(fp_mul(dx, p->dir_y) - + fp_mul(dy, p->dir_x)); + + if (cross < FP_HALF) { + e->health -= 20; + doom_play_sound(snd_hit); + + if (e->health <= 0) { + e->state = ESTATE_DYING; + e->anim_tick = 0; + game.score += (e->type == ENEMY_DEMON) ? 200 : 100; + } else { + e->state = ESTATE_CHASE; + } + break; + } + } +} + +static void update_enemies(void) +{ + struct doom_player *p = &game.player; + + for (int i = 0; i < game.num_enemies; i++) { + struct doom_enemy *e = &game.enemies[i]; + if (e->type == ENEMY_NONE) continue; + + switch (e->state) { + case ESTATE_IDLE: { + fixed_t dx = e->x - p->x; + fixed_t dy = e->y - p->y; + fixed_t dist = fp_mul(dx, dx) + fp_mul(dy, dy); + if (dist < fp_from_int(6 * 6)) { + e->state = ESTATE_CHASE; + } + break; + } + case ESTATE_CHASE: { + fixed_t dx = p->x - e->x; + fixed_t dy = p->y - e->y; + fixed_t dist = fp_mul(dx, dx) + fp_mul(dy, dy); + + if (dist < fp_from_int(2)) { + e->state = ESTATE_ATTACK; + e->anim_tick = 0; + } else { + fixed_t speed = FP_ONE / 16; + fixed_t move_x = fp_mul(dx, speed) / + (fp_to_int(dist) + 1); + fixed_t move_y = fp_mul(dy, speed) / + (fp_to_int(dist) + 1); + + fixed_t nx = e->x + move_x; + fixed_t ny = e->y + move_y; + + if (doom_map_get(fp_to_int(nx), fp_to_int(ny)) == 0) { + e->x = nx; + e->y = ny; + } + } + break; + } + case ESTATE_ATTACK: + e->anim_tick++; + if (e->anim_tick >= 20) { + int dmg = (e->type == ENEMY_DEMON) ? 15 : 8; + p->health -= dmg; + doom_play_sound(snd_hit); + + if (p->health <= 0) { + game.game_over = true; + doom_play_sound(snd_death); + } + e->anim_tick = 0; + e->state = ESTATE_CHASE; + } + break; + case ESTATE_DYING: + e->anim_tick++; + if (e->anim_tick >= 10) { + e->state = ESTATE_DEAD; + } + break; + case ESTATE_DEAD: + break; + } + } +} + +/* ========== GAME TICK (work handler) ========== */ + +static void game_tick_handler(struct k_work *work) +{ + ARG_UNUSED(work); + + if (!running) return; + + uint32_t input = (uint32_t)atomic_get(&input_state); + + if (game.game_over) { + draw_game_over(); + doom_flush_fb(); + + if (input & DINPUT_FIRE) { + game_init(); + doom_sound_off(); + } + + /* Reschedule at slower rate for game over screen */ + k_work_reschedule(&game_tick_work, K_MSEC(100)); + return; + } + + handle_movement(input); + + if (fire_cooldown > 0) { + fire_cooldown--; + } else if (input & DINPUT_FIRE) { + handle_shooting(input); + fire_cooldown = 4; + } + if (!(input & DINPUT_FIRE)) { + game.player.firing = false; + } + + update_enemies(); + + memset(render_fb, 0, FB_SIZE); + raycaster_render(); + draw_gun(game.player.firing && fire_cooldown > 2); + draw_hud(); + doom_flush_fb(); + + game.frame_count++; + + /* Reschedule next tick */ + k_work_reschedule(&game_tick_work, K_MSEC(FRAME_MS)); +} + +/* ========== PUBLIC API ========== */ + +void doom_game_start(void) +{ + if (running) return; + + LOG_INF("Doom easter egg starting!"); + + /* Ensure display is unblanked */ + const struct device *dev = mc_display_get_device(); + if (dev) { + display_blanking_off(dev); + } + +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + /* Stop any firmware buzzer activity */ + buzzer_stop(); +#endif + + game_init(); + running = true; + + k_work_init_delayable(&game_tick_work, game_tick_handler); + k_work_init_delayable(&sound_work, doom_sound_work_handler); + + /* Start game loop */ + k_work_reschedule(&game_tick_work, K_NO_WAIT); +} + +void doom_game_stop(void) +{ + if (!running) return; + + LOG_INF("Doom easter egg stopping"); + + running = false; + k_work_cancel_delayable(&game_tick_work); + doom_sound_off(); + + /* Clear the direct framebuffer to black */ + const struct device *dev = mc_display_get_device(); + if (dev) { + memset(render_fb, 0, FB_SIZE); + struct display_buffer_descriptor desc = { + .buf_size = FB_SIZE, + .width = SCREEN_W, + .height = SCREEN_H, + .pitch = SCREEN_W, + }; + display_write(dev, 0, 0, &desc, render_fb); + } +} + +bool doom_game_is_running(void) +{ + return running; +} + +void doom_game_input(uint16_t code, int32_t value) +{ + uint32_t bit = 0; + + switch (code) { + case INPUT_KEY_UP: + bit = DINPUT_FWD; + break; + case INPUT_KEY_DOWN: + bit = DINPUT_BACK; + break; + case INPUT_KEY_LEFT: + bit = DINPUT_RIGHT; + break; + case INPUT_KEY_RIGHT: + bit = DINPUT_LEFT; + break; + case INPUT_KEY_ENTER: + case INPUT_KEY_0: + bit = DINPUT_FIRE; + break; + default: + return; + } + + if (value) { + atomic_or(&input_state, bit); + } else { + atomic_and(&input_state, ~bit); + } +} diff --git a/zephcore/helpers/ui/doom_game.h b/zephcore/helpers/ui/doom_game.h new file mode 100644 index 0000000..68f983c --- /dev/null +++ b/zephcore/helpers/ui/doom_game.h @@ -0,0 +1,69 @@ +/* + * ZephCore - Doom Raycaster Easter Egg + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Hidden easter egg: Wolf3D-style raycaster on the OLED display. + * Activated by double-click + single-click joystick center on page 0. + * Deactivated by double-click joystick center. + * + * Build-time control: CONFIG_ZEPHCORE_EASTER_EGG_DOOM + */ + +#ifndef ZEPHCORE_DOOM_GAME_H +#define ZEPHCORE_DOOM_GAME_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef CONFIG_ZEPHCORE_EASTER_EGG_DOOM + +/** + * Start the Doom easter egg game. + * Takes over display and input until doom_game_stop() is called. + */ +void doom_game_start(void); + +/** + * Stop the Doom easter egg game. + * Restores display to normal firmware UI. + */ +void doom_game_stop(void); + +/** + * Check if Doom is currently running. + * Used by display and input guards to redirect I/O. + */ +bool doom_game_is_running(void); + +/** + * Forward an input event to the Doom game. + * Called from ui_input_cb() when game is running. + * + * @param code Input key code (INPUT_KEY_UP, etc.) + * @param value 1 = pressed, 0 = released + */ +void doom_game_input(uint16_t code, int32_t value); + +#else /* !CONFIG_ZEPHCORE_EASTER_EGG_DOOM */ + +static inline void doom_game_start(void) {} +static inline void doom_game_stop(void) {} +static inline bool doom_game_is_running(void) { return false; } +static inline void doom_game_input(uint16_t code, int32_t value) +{ + (void)code; + (void)value; +} + +#endif /* CONFIG_ZEPHCORE_EASTER_EGG_DOOM */ + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHCORE_DOOM_GAME_H */ diff --git a/zephcore/helpers/ui/input_multi_tap.c b/zephcore/helpers/ui/input_multi_tap.c new file mode 100644 index 0000000..4cc99ef --- /dev/null +++ b/zephcore/helpers/ui/input_multi_tap.c @@ -0,0 +1,170 @@ +/* + * Copyright 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Multi-tap input filter. + * + * Counts rapid taps within a configurable window and emits a different + * key code based on the tap count. Unlike the old triple-tap filter + * (which emitted immediately at count==3), this filter ALWAYS waits for + * the tap window to expire before emitting — except when the maximum + * tap count is reached, in which case it emits immediately. + * + * This means single-tap has ~tap-delay-ms latency, which is acceptable + * for UI actions like page-next. + */ + +#define DT_DRV_COMPAT zephcore_input_multi_tap + +#include +#include +#include + +#include +LOG_MODULE_REGISTER(input_multi_tap, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +struct multi_tap_config { + const struct device *input_dev; + const uint16_t *input_codes; + const uint16_t *tap_codes; + uint32_t tap_delay_ms; + uint8_t num_input_codes; + uint8_t num_tap_codes; /* max taps recognized (length of tap_codes) */ +}; + +struct multi_tap_data { + const struct device *dev; + struct k_work_delayable work; + uint8_t tap_count; +}; + +static void multi_tap_emit(struct multi_tap_data *data) +{ + const struct device *dev = data->dev; + const struct multi_tap_config *cfg = dev->config; + + if (data->tap_count > 0 && data->tap_count <= cfg->num_tap_codes) { + uint16_t code = cfg->tap_codes[data->tap_count - 1]; + + LOG_INF("multi-tap: %u tap(s) -> emit code %u", data->tap_count, code); + input_report_key(dev, code, 1, true, K_FOREVER); + input_report_key(dev, code, 0, true, K_FOREVER); + } + data->tap_count = 0; +} + +static void multi_tap_deferred(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct multi_tap_data *data = + CONTAINER_OF(dwork, struct multi_tap_data, work); + + /* Window expired — emit code for the tap count we saw */ + multi_tap_emit(data); +} + +static void __maybe_unused multi_tap_cb(struct input_event *evt, void *user_data) +{ + const struct device *dev = user_data; + const struct multi_tap_config *cfg = dev->config; + struct multi_tap_data *data = dev->data; + + if (evt->type != INPUT_EV_KEY) { + return; + } + + /* Only count key-press events (value=1), ignore releases */ + if (!evt->value) { + return; + } + + LOG_DBG("code=%u (watching %u codes, first=%u)", + evt->code, cfg->num_input_codes, + cfg->num_input_codes > 0 ? cfg->input_codes[0] : 0); + + /* Check if this is a watched input code */ + bool found = false; + + for (int i = 0; i < cfg->num_input_codes; i++) { + if (evt->code == cfg->input_codes[i]) { + found = true; + break; + } + } + if (!found) { + return; + } + + data->tap_count++; + LOG_INF("multi-tap: tap %u/%u", data->tap_count, cfg->num_tap_codes); + + if (data->tap_count >= cfg->num_tap_codes) { + /* Reached max tap count — emit immediately, no point waiting */ + k_work_cancel_delayable(&data->work); + multi_tap_emit(data); + } else { + /* Restart window timer */ + k_work_reschedule(&data->work, K_MSEC(cfg->tap_delay_ms)); + } +} + +static int __maybe_unused multi_tap_init(const struct device *dev) +{ + const struct multi_tap_config *cfg = dev->config; + struct multi_tap_data *data = dev->data; + + if (cfg->input_dev && !device_is_ready(cfg->input_dev)) { + LOG_ERR("input device not ready"); + return -ENODEV; + } + + data->dev = dev; + data->tap_count = 0; + k_work_init_delayable(&data->work, multi_tap_deferred); + + LOG_INF("multi-tap init: input_dev=%p, %u input codes, %u tap codes, delay=%u ms", + cfg->input_dev, cfg->num_input_codes, cfg->num_tap_codes, cfg->tap_delay_ms); + + return 0; +} + +#define INPUT_MULTI_TAP_DEFINE(inst) \ + BUILD_ASSERT(DT_INST_PROP_LEN(inst, input_codes) >= 1, \ + "input-codes must have at least 1 entry"); \ + BUILD_ASSERT(DT_INST_PROP_LEN(inst, tap_codes) >= 1, \ + "tap-codes must have at least 1 entry"); \ + BUILD_ASSERT(DT_INST_PROP_LEN(inst, tap_codes) <= 4, \ + "tap-codes must have at most 4 entries"); \ + \ + INPUT_CALLBACK_DEFINE_NAMED( \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, input), \ + (DEVICE_DT_GET_OR_NULL(DT_INST_PHANDLE(inst, input))), \ + (NULL)), \ + multi_tap_cb, (void *)DEVICE_DT_INST_GET(inst), \ + multi_tap_cb_##inst); \ + \ + static const uint16_t multi_tap_input_codes_##inst[] = \ + DT_INST_PROP(inst, input_codes); \ + \ + static const uint16_t multi_tap_codes_##inst[] = \ + DT_INST_PROP(inst, tap_codes); \ + \ + static struct multi_tap_data multi_tap_data_##inst; \ + \ + static const struct multi_tap_config multi_tap_config_##inst = { \ + .input_dev = COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, input), \ + (DEVICE_DT_GET_OR_NULL(DT_INST_PHANDLE(inst, input))), \ + (NULL)), \ + .input_codes = multi_tap_input_codes_##inst, \ + .tap_codes = multi_tap_codes_##inst, \ + .tap_delay_ms = DT_INST_PROP(inst, tap_delay_ms), \ + .num_input_codes = DT_INST_PROP_LEN(inst, input_codes), \ + .num_tap_codes = DT_INST_PROP_LEN(inst, tap_codes), \ + }; \ + \ + DEVICE_DT_INST_DEFINE(inst, multi_tap_init, NULL, \ + &multi_tap_data_##inst, \ + &multi_tap_config_##inst, \ + POST_KERNEL, CONFIG_INPUT_INIT_PRIORITY, NULL); + +DT_INST_FOREACH_STATUS_OKAY(INPUT_MULTI_TAP_DEFINE) diff --git a/zephcore/helpers/ui/ui_mesh_actions.cpp b/zephcore/helpers/ui/ui_mesh_actions.cpp new file mode 100644 index 0000000..f46a0b2 --- /dev/null +++ b/zephcore/helpers/ui/ui_mesh_actions.cpp @@ -0,0 +1,291 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore UI ↔ Mesh Action Helpers + * + * Deferred UI button actions and periodic housekeeping display refresh. + * Extracted from main_companion.cpp. + * + * This is a .cpp file because it accesses C++ mesh objects (CompanionMesh, + * ZephyrDataStore, LoRaRadioBase, ZephyrBoard, ZephyrRTCClock). + * The extern "C" wrappers are called from ui_task.c (C code). + */ + +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_ui_actions, CONFIG_ZEPHCORE_UI_ACTIONS_LOG_LEVEL); + +#include +#include +#include +#include +#include +#include +#include +#include "ui_task.h" +#include "ui_mesh_actions.h" + +/* UI action bit flags — set from input thread, consumed by mesh event loop */ +#define UI_ACTION_FLOOD_ADVERT BIT(0) +#define UI_ACTION_GPS_TOGGLE BIT(1) +#define UI_ACTION_BUZZER_TOGGLE BIT(2) +#define UI_ACTION_ZEROHOP_ADVERT BIT(3) +#define UI_ACTION_OFFGRID_TOGGLE BIT(4) + +/* Module-local pointers, set by init */ +static CompanionMesh *s_mesh; +static ZephyrDataStore *s_data_store; +static mesh::LoRaRadioBase *s_lora_radio; +static mesh::ZephyrBoard *s_board; +static mesh::ZephyrRTCClock *s_rtc_clock; +static struct k_event *s_mesh_events; +static uint32_t s_mesh_event_ui_action; + +/* Atomic UI action flags — set from input thread, consumed in mesh event loop */ +static atomic_t pending_ui_actions; + +/* Pending state for toggle actions (pass the value to mesh thread) */ +static volatile bool pending_gps_enabled; +static volatile bool pending_buzzer_quiet; +static volatile bool pending_offgrid_enabled; + +extern "C" void ui_mesh_actions_init(struct k_event *mesh_events, + uint32_t mesh_event_ui_action, + void *companion_mesh, void *data_store, + void *lora_radio, void *zephyr_board, + void *rtc_clock) +{ + s_mesh_events = mesh_events; + s_mesh_event_ui_action = mesh_event_ui_action; + s_mesh = static_cast(companion_mesh); + s_data_store = static_cast(data_store); + s_lora_radio = static_cast(lora_radio); + s_board = static_cast(zephyr_board); + s_rtc_clock = static_cast(rtc_clock); +} + +/* ========== Button action wrappers (called from input thread) ========== */ + +extern "C" void mesh_send_flood_advert(void) +{ + atomic_or(&pending_ui_actions, UI_ACTION_FLOOD_ADVERT); + k_event_post(s_mesh_events, s_mesh_event_ui_action); +} + +extern "C" void mesh_send_zerohop_advert(void) +{ + atomic_or(&pending_ui_actions, UI_ACTION_ZEROHOP_ADVERT); + k_event_post(s_mesh_events, s_mesh_event_ui_action); +} + +extern "C" void mesh_gps_set_enabled(bool enable) +{ + /* Toggle GPS hardware immediately (lightweight, no flash) */ + gps_enable(enable); + + /* Defer the flash write (savePrefs) to mesh thread */ + pending_gps_enabled = enable; + atomic_or(&pending_ui_actions, UI_ACTION_GPS_TOGGLE); + k_event_post(s_mesh_events, s_mesh_event_ui_action); +} + +extern "C" void mesh_ble_set_enabled(bool enable) +{ + zephcore_ble_set_enabled(enable); +} + +extern "C" void mesh_set_buzzer_quiet(bool quiet) +{ + /* Defer the flash write (savePrefs) to mesh thread */ + pending_buzzer_quiet = quiet; + atomic_or(&pending_ui_actions, UI_ACTION_BUZZER_TOGGLE); + k_event_post(s_mesh_events, s_mesh_event_ui_action); +} + +extern "C" void mesh_set_offgrid_mode(bool enable) +{ + /* Defer the flash write (savePrefs) to mesh thread */ + pending_offgrid_enabled = enable; + atomic_or(&pending_ui_actions, UI_ACTION_OFFGRID_TOGGLE); + k_event_post(s_mesh_events, s_mesh_event_ui_action); +} + +/* Disable power regulators for System OFF. + * Only touches sensor power and buzzer power-gate regulators. + * GPS is handled separately by gps_power_off_for_shutdown(). + * DO NOT touch BLE here — that corrupts controller state across reset. */ +extern "C" void mesh_disable_power_regulators(void) +{ +#if DT_NODE_EXISTS(DT_NODELABEL(sensor_power)) + const struct device *sensor_reg = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sensor_power)); + if (sensor_reg && device_is_ready(sensor_reg)) { + regulator_disable(sensor_reg); + } +#endif + +#if DT_NODE_EXISTS(DT_NODELABEL(buzzer_enable)) + const struct device *buzz_reg = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(buzzer_enable)); + if (buzz_reg && device_is_ready(buzz_reg)) { + regulator_disable(buzz_reg); + } +#endif +} + +extern "C" void mesh_reboot_to_ota_dfu(void) +{ + char reply[64]; + s_board->startOTAUpdate(nullptr, reply); + /* Never returns */ +} + +/* ========== Deferred action processing (runs on mesh thread) ========== */ + +extern "C" void mesh_handle_ui_actions(void) +{ + uint32_t actions = atomic_clear(&pending_ui_actions); + + if (!s_mesh || actions == 0) { + return; + } + + if (actions & (UI_ACTION_FLOOD_ADVERT | UI_ACTION_ZEROHOP_ADVERT)) { + bool flood = !!(actions & UI_ACTION_FLOOD_ADVERT); + mesh::Packet *adv; + if (s_mesh->prefs.advert_loc_policy == 0) { + adv = s_mesh->createSelfAdvert( + s_mesh->prefs.node_name); + } else { + adv = s_mesh->createSelfAdvert( + s_mesh->prefs.node_name, + s_mesh->prefs.node_lat, + s_mesh->prefs.node_lon); + } + if (adv) { + if (flood) { + s_mesh->sendFlood(adv); + LOG_INF("flood advert sent (button)"); + } else { + s_mesh->sendZeroHop(adv); + LOG_INF("zero-hop advert sent (button)"); + } + } + } + + /* Save prefs if any toggle action changed them */ + bool need_save = false; + + if (actions & UI_ACTION_GPS_TOGGLE) { + s_mesh->prefs.gps_enabled = + pending_gps_enabled ? 1 : 0; + LOG_INF("GPS %s (button)", pending_gps_enabled ? "on" : "off"); + need_save = true; + } + + if (actions & UI_ACTION_BUZZER_TOGGLE) { + s_mesh->prefs.buzzer_quiet = + pending_buzzer_quiet ? 1 : 0; + LOG_INF("buzzer_quiet=%d (button)", pending_buzzer_quiet); + need_save = true; + } + + if (actions & UI_ACTION_OFFGRID_TOGGLE) { + s_mesh->prefs.client_repeat = + pending_offgrid_enabled ? 1 : 0; + LOG_INF("client_repeat=%d (button)", pending_offgrid_enabled); + need_save = true; + } + + if (need_save) { + s_data_store->savePrefs(s_mesh->prefs); + LOG_INF("prefs saved (button action)"); + } +} + +/* ========== Periodic housekeeping UI refresh ========== */ + +extern "C" void mesh_housekeeping_ui_refresh(void) +{ + if (!s_mesh) { + return; + } + + /* Battery voltage changes slowly — read every ~60s (12 × 5s housekeeping) + * instead of every 5s. Saves power (regulator toggle + ADC) and log noise. */ + static uint8_t batt_counter; + if (++batt_counter >= 12) { + batt_counter = 0; + ui_set_battery(s_board->getBattMilliVolts(), 0); + } + + /* Update top bar clock from RTC */ + ui_set_clock(s_rtc_clock->getCurrentTime()); + + ui_set_radio_params( + (uint32_t)(s_mesh->prefs.freq * 1000000.0f + 0.5f), + s_mesh->prefs.sf, + (uint16_t)(s_mesh->prefs.bw * 10.0f + 0.5f), + s_mesh->prefs.cr, + s_mesh->prefs.tx_power_dbm, + s_lora_radio->getNoiseFloor()); + + /* Update GPS satellite count even without fix */ + if (gps_is_enabled()) { + struct gps_position gpos; + + gps_get_position(&gpos); + if (!gpos.valid) { + ui_set_gps_data(false, (uint8_t)gpos.satellites, + 0, 0, 0); + } + } + + /* Update GPS state machine info (state, last fix age, next search) */ + { + struct gps_state_info gsi; + + gps_get_state_info(&gsi); + ui_set_gps_state(gsi.state, gsi.last_fix_age_s, gsi.next_search_s); + } + + /* Update recently heard contacts from mesh advert path table. + * getRecentlyHeard() returns the N most recent entries sorted + * descending by recv_timestamp (newest first). + * We compute age_s from RTC timestamps and pass to UI. */ + { + AdvertPath recent_adverts[4]; + int num = s_mesh->getRecentlyHeard(recent_adverts, 4); + uint32_t now_rtc = s_rtc_clock->getCurrentTime(); + + ui_clear_recent(); + for (int i = num - 1; i >= 0; i--) { + if (recent_adverts[i].name[0]) { + /* Guard: if recv_timestamp > now_rtc (clock went + * backwards after time sync change), clamp to 0. */ + uint32_t age_s = 0; + if (now_rtc >= recent_adverts[i].recv_timestamp) { + age_s = now_rtc - recent_adverts[i].recv_timestamp; + } + ui_add_recent(recent_adverts[i].name, 0, age_s); + } + } + } + + /* Read environment sensors if available */ + if (env_sensors_available()) { + struct env_data edata; + + if (env_sensors_read(&edata) == 0) { + ui_set_sensor_data( + edata.has_temperature ? (int16_t)(edata.temperature_c * 10) : 0, + edata.has_pressure ? (uint32_t)(edata.pressure_hpa * 100) : 0, + edata.has_humidity ? (uint16_t)(edata.humidity_pct * 10) : 0, + 0); + } + } + + /* Trigger display refresh */ + ui_refresh_display(); +} diff --git a/zephcore/helpers/ui/ui_mesh_actions.h b/zephcore/helpers/ui/ui_mesh_actions.h new file mode 100644 index 0000000..e3592e4 --- /dev/null +++ b/zephcore/helpers/ui/ui_mesh_actions.h @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore UI ↔ Mesh Action Helpers + * + * Deferred UI button actions and periodic housekeeping display refresh. + * Called from the mesh event loop thread (safe for LoRa TX, flash writes). + * + * The extern "C" functions (mesh_send_flood_advert, etc.) are called from + * ui_task.c button handlers in the input thread context. + */ +#ifndef ZEPHCORE_UI_MESH_ACTIONS_H +#define ZEPHCORE_UI_MESH_ACTIONS_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the UI mesh actions module. + * Must be called after companion_mesh is constructed and before the event loop. + * + * @param mesh_events Pointer to the mesh k_event + * @param mesh_event_ui_action Bitmask for MESH_EVENT_UI_ACTION + * @param companion_mesh Opaque pointer to CompanionMesh (cast internally) + * @param data_store Opaque pointer to ZephyrDataStore + * @param lora_radio Opaque pointer to LoRa radio adapter + * @param zephyr_board Opaque pointer to ZephyrBoard + * @param rtc_clock Opaque pointer to ZephyrRTCClock + */ +void ui_mesh_actions_init(struct k_event *mesh_events, uint32_t mesh_event_ui_action, + void *companion_mesh, void *data_store, + void *lora_radio, void *zephyr_board, void *rtc_clock); + +/** + * Process deferred UI actions — called from mesh event loop on MESH_EVENT_UI_ACTION. + * Safe to access LoRa radio, flash, and CompanionMesh from here. + */ +void mesh_handle_ui_actions(void); + +/** + * Periodic UI display refresh — called from mesh event loop on MESH_EVENT_HOUSEKEEPING. + * Updates battery, noise floor, GPS, recently heard, and sensors. + */ +void mesh_housekeeping_ui_refresh(void); + +/* + * Button action wrappers — called from ui_task.c (C code). + * These run in the input thread. Heavy work is deferred to mesh thread + * via atomic flags + MESH_EVENT_UI_ACTION. + */ +void mesh_send_flood_advert(void); +void mesh_send_zerohop_advert(void); +void mesh_gps_set_enabled(bool enable); +void mesh_ble_set_enabled(bool enable); +void mesh_set_buzzer_quiet(bool quiet); +void mesh_set_offgrid_mode(bool enable); +void mesh_disable_power_regulators(void); +void mesh_reboot_to_ota_dfu(void); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHCORE_UI_MESH_ACTIONS_H */ diff --git a/zephcore/helpers/ui/ui_pages.c b/zephcore/helpers/ui/ui_pages.c new file mode 100644 index 0000000..621e321 --- /dev/null +++ b/zephcore/helpers/ui/ui_pages.c @@ -0,0 +1,694 @@ +/* + * ZephCore - UI Page Renderers + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Multi-page UI system matching Arduino's ui-new HomeScreen implementation. + * Each page renders via the display abstraction (any resolution/display type). + * + * Layout is resolution-independent — all positions derived from actual + * display dimensions and font size queried at runtime: + * Top bar: Node name left, battery right + * Dots row: Page indicator dots (centered) + * Separator: Horizontal line + * Content: Page-specific rendering + */ + +#include "ui_pages.h" +#include "display.h" + +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(ui_pages, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +/* ZephCore logo bitmap - 128x13px, MSB-first (Adafruit drawBitmap format) */ +static const uint8_t zephcore_logo[] = { + 0x00, 0x01, 0xff, 0x7f, 0xe7, 0xf8, 0x70, 0x70, + 0x3c, 0x01, 0xe0, 0x7f, 0xc3, 0xff, 0x00, 0x00, + 0x00, 0x01, 0xff, 0x7f, 0xe7, 0xfc, 0x70, 0x70, + 0xff, 0x07, 0xf8, 0x7f, 0xe3, 0xff, 0x00, 0x00, + 0x00, 0x01, 0xff, 0x7f, 0xe7, 0xfe, 0x70, 0x71, + 0xff, 0x0f, 0xfc, 0x7f, 0xf3, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x0e, 0x70, 0x07, 0x0e, 0x70, 0x71, + 0xc7, 0x8e, 0x1c, 0x70, 0x73, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x1c, 0x70, 0x07, 0x0e, 0x70, 0x73, + 0x83, 0x1c, 0x0e, 0x70, 0x73, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x38, 0x7f, 0xe7, 0xfe, 0x7f, 0xf3, + 0x80, 0x1c, 0x0e, 0x7f, 0xf3, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x78, 0x7f, 0xe7, 0xfc, 0x7f, 0xf3, + 0x80, 0x1c, 0x0e, 0x7f, 0xe3, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x70, 0x7f, 0xe7, 0xf8, 0x7f, 0xf3, + 0x80, 0x1c, 0x0e, 0x7f, 0x83, 0xff, 0x00, 0x00, + 0x00, 0x00, 0xe0, 0x70, 0x07, 0x00, 0x70, 0x73, + 0x83, 0x1c, 0x0e, 0x73, 0xc3, 0x80, 0x00, 0x00, + 0x00, 0x01, 0xc0, 0x70, 0x07, 0x00, 0x70, 0x71, + 0xc7, 0x8e, 0x1c, 0x71, 0xe3, 0x80, 0x00, 0x00, + 0x00, 0x03, 0xff, 0x7f, 0xe7, 0x00, 0x70, 0x71, + 0xff, 0x0f, 0xfc, 0x70, 0xe3, 0xff, 0x00, 0x00, + 0x00, 0x03, 0xff, 0x7f, 0xe7, 0x00, 0x70, 0x70, + 0xff, 0x07, 0xf8, 0x70, 0xf3, 0xff, 0x00, 0x00, + 0x00, 0x03, 0xff, 0x7f, 0xe7, 0x00, 0x70, 0x70, + 0x3c, 0x01, 0xe0, 0x70, 0x7b, 0xff, 0x00, 0x00, +}; + +/* ========== UTF-8 to Latin-1 Sanitizer ========== */ +/* Node names may contain UTF-8 emojis or accented chars. + * Our font covers 32-255 (ASCII + Latin-1 Supplement). + * This function: + * - Passes ASCII (0x00-0x7F) unchanged + * - Converts 2-byte Latin-1 sequences (U+00A0-U+00FF) to raw bytes + * - Strips all other multi-byte sequences (emojis, CJK, etc.) + * - Strips leading/trailing whitespace from removed chars + */ +void utf8_to_latin1(char *dst, const char *src, size_t dst_size) +{ + size_t di = 0; + size_t si = 0; + + while (src[si] && di < dst_size - 1) { + uint8_t c = (uint8_t)src[si]; + + if (c < 0x80) { + /* ASCII byte — pass through */ + dst[di++] = (char)c; + si++; + } else if ((c & 0xE0) == 0xC0) { + /* 2-byte UTF-8: 110xxxxx 10xxxxxx */ + uint8_t c2 = (uint8_t)src[si + 1]; + + if ((c2 & 0xC0) != 0x80) { + /* Invalid continuation — skip lead byte */ + si++; + continue; + } + + uint16_t cp = ((c & 0x1F) << 6) | (c2 & 0x3F); + + if (cp >= 0xA0 && cp <= 0xFF) { + /* Latin-1 Supplement — fits in our font */ + dst[di++] = (char)(uint8_t)cp; + } + /* else: Latin Extended etc — skip */ + si += 2; + } else if ((c & 0xF0) == 0xE0) { + /* 3-byte UTF-8 — skip (BMP: CJK, symbols) */ + si += 3; + } else if ((c & 0xF8) == 0xF0) { + /* 4-byte UTF-8 — skip (emojis, supplementary) */ + si += 4; + } else { + /* Invalid/continuation byte — skip */ + si++; + } + } + dst[di] = '\0'; + + /* Strip leading spaces (from removed emoji prefix) */ + size_t start = 0; + + while (dst[start] == ' ') { + start++; + } + if (start > 0) { + memmove(dst, dst + start, di - start + 1); + } +} + +/* ========== Layout ========== */ +/* All layout is derived from the actual display and font dimensions + * queried at runtime, so the UI adapts to any resolution/font size. + * + * Naming convention: UPPER_CASE macros call display getters (cheap — + * they just return a cached static). This keeps page renderers + * readable while being resolution-independent. + * + * Layout (top to bottom): + * Row 0..FONT_H-1: Top bar (node name left, battery right) + * DOTS_Y..SEP_Y: Page indicator dots + separator line + * CONTENT_Y..end: Page-specific content + */ +#define FONT_W mc_display_font_width() +#define FONT_H mc_display_font_height() +#define DISP_W mc_display_width() +#define DISP_H mc_display_height() +#define LINE_H (FONT_H + 2) /* font height + 2px gap */ +#define TOP_BAR_Y 0 +#define DOTS_Y (FONT_H + 2) /* just below top bar */ +#define SEP_Y (DOTS_Y + 4) /* below dots */ +#define CONTENT_Y (SEP_Y + 4) /* below separator */ +#define MAX_CHARS (DISP_W / (FONT_W ? FONT_W : 1)) + +/* ========== Global State ========== */ +static struct ui_state state; + +/* ========== Helper: Battery Percentage from mV ========== */ + +static uint8_t calc_battery_pct(uint16_t mv) +{ + if (mv >= 4200) { + return 100; + } + if (mv <= 3000) { + return 0; + } + return (uint8_t)((mv - 3000) * 100 / 1200); +} + +/* ========== Helper: Top Bar (Node Name + Battery) ========== */ + +static void render_top_bar(void) +{ + /* Node name on the left */ + if (state.node_name[0]) { + /* Truncate to fit left side (leave room for battery) */ + char name[16]; + + strncpy(name, state.node_name, sizeof(name) - 1); + name[sizeof(name) - 1] = '\0'; + mc_display_text(0, TOP_BAR_Y, name, false); + } + + /* Right side: "HH:MM XX%" — clock then battery, right-aligned */ + { + char right[16] = ""; + int pos = 0; + + /* 24h clock from RTC epoch (only if time has been synced). + * Before sync, getCurrentTime() returns bare uptime (~seconds), + * so check for a sane epoch (after Jan 1 2025 = 1735689600). */ + if (state.rtc_epoch > 1735689600) { + uint32_t day_sec = state.rtc_epoch % 86400; + uint8_t hh = day_sec / 3600; + uint8_t mm = (day_sec % 3600) / 60; + + pos = snprintf(right, sizeof(right), "%02u:%02u ", hh, mm); + } + + /* Battery percentage */ + if (state.battery_mv > 0) { + uint8_t pct = state.battery_pct; + + if (pct == 0) { + pct = calc_battery_pct(state.battery_mv); + } + snprintf(right + pos, sizeof(right) - pos, "%u%%", pct); + } + + if (right[0]) { + int x = DISP_W - ((int)strlen(right) * FONT_W); + + mc_display_text(x, TOP_BAR_Y, right, false); + } + } +} + +/* ========== Helper: Page Indicator Dots ========== */ + +static void render_page_indicator(void) +{ + /* Centered dots matching Arduino layout: + * Each dot spaced 10px apart, centered on screen. + * Current page = filled 3x3 block, others = single pixel. */ + int total = UI_PAGE_COUNT; + int dot_spacing = 10; + int total_width = (total - 1) * dot_spacing; + int start_x = (DISP_W - total_width) / 2; + + for (int i = 0; i < total; i++) { + int x = start_x + i * dot_spacing; + + if (i == (int)state.current_page) { + /* Current page: filled 3x3 block */ + mc_display_fill_rect(x - 1, DOTS_Y, 3, 3); + } else { + /* Other pages: single pixel */ + mc_display_fill_rect(x, DOTS_Y + 1, 1, 1); + } + } + + /* Separator line below dots */ + mc_display_hline(0, SEP_Y, DISP_W); +} + +/* ========== Helper: Center text ========== */ + +static void draw_centered(int y, const char *text) +{ + int len = (int)strlen(text); + int x = (DISP_W - (len * FONT_W)) / 2; + + if (x < 0) { + x = 0; + } + mc_display_text(x, y, text, false); +} + +/* ========== Page Renderers ========== */ + +static void render_messages(void) +{ + /* Large centered message count (matching Arduino "MSG: X") */ + char buf[24]; + + snprintf(buf, sizeof(buf), "MSG: %u", state.msg_count); + draw_centered(CONTENT_Y + 2, buf); + + /* Connection status centered below */ + if (state.ble_connected) { + draw_centered(CONTENT_Y + 16, "< Connected >"); + } else { + draw_centered(CONTENT_Y + 16, "Waiting for app..."); + } + + /* Offgrid mode status */ + snprintf(buf, sizeof(buf), "Offgrid: %s", + state.offgrid_enabled ? "ON" : "OFF"); + draw_centered(CONTENT_Y + 32, buf); +} + +static void render_recent(void) +{ + if (state.recent_count == 0) { + draw_centered(CONTENT_Y + 10, "(no contacts heard)"); + return; + } + + int y = CONTENT_Y; + + for (int i = 0; i < state.recent_count && i < 4; i++) { + /* age_s is recomputed from RTC timestamps each housekeeping + * cycle (~5s), so it's always fresh and monotonic. */ + uint32_t age_s = state.recent[i].age_s; + + /* Format elapsed time */ + char time_str[8]; + + if (age_s < 60) { + snprintf(time_str, sizeof(time_str), "%us", age_s); + } else if (age_s < 3600) { + snprintf(time_str, sizeof(time_str), "%um", age_s / 60); + } else if (age_s < 86400) { + snprintf(time_str, sizeof(time_str), "%uh", age_s / 3600); + } else { + snprintf(time_str, sizeof(time_str), "%ud", age_s / 86400); + } + + /* Name left-aligned, time right-aligned */ + char buf[24]; + + snprintf(buf, sizeof(buf), "%-13s %s", + state.recent[i].name, time_str); + mc_display_text(0, y, buf, false); + y += LINE_H; + } +} + +static void render_radio(void) +{ + char buf[28]; + int y = CONTENT_Y; + + /* Line 1: FQ, SF, BW */ + uint32_t freq_mhz = state.lora_freq_hz / 1000000; + uint32_t freq_frac = (state.lora_freq_hz % 1000000 + 500) / 1000; + uint16_t bw_int = state.lora_bw_khz_x10 / 10; + uint16_t bw_frac = state.lora_bw_khz_x10 % 10; + + if (bw_frac) { + snprintf(buf, sizeof(buf), "%u.%03u SF%u BW%u.%u", + freq_mhz, freq_frac, state.lora_sf, bw_int, bw_frac); + } else { + snprintf(buf, sizeof(buf), "%u.%03u SF%u BW%u", + freq_mhz, freq_frac, state.lora_sf, bw_int); + } + mc_display_text(0, y, buf, false); + y += LINE_H; + + /* Line 2: CR, TX Power */ + snprintf(buf, sizeof(buf), "CR:%u TX:%ddBm", state.lora_cr, state.lora_tx_power); + mc_display_text(0, y, buf, false); + y += LINE_H; + + /* Line 3: Noise floor */ + snprintf(buf, sizeof(buf), "Noise: %ddBm", state.lora_noise_floor); + mc_display_text(0, y, buf, false); + y += LINE_H; + + /* Uptime */ + uint32_t up_s = (uint32_t)(k_uptime_get() / 1000); + uint32_t days = up_s / 86400; + uint32_t hours = (up_s % 86400) / 3600; + uint32_t mins = (up_s % 3600) / 60; + + snprintf(buf, sizeof(buf), "Up: %ud %uh %um", days, hours, mins); + mc_display_text(0, y, buf, false); +} + +static void render_bluetooth(void) +{ + if (!state.ble_enabled) { + draw_centered(CONTENT_Y + 10, "BLE: OFF"); + draw_centered(CONTENT_Y + 28, "Press to Enable"); + return; + } + + if (state.ble_connected) { + draw_centered(CONTENT_Y + 4, "BLE: Connected"); + draw_centered(CONTENT_Y + 20, "Press to Disable"); + } else { + draw_centered(CONTENT_Y + 4, "BLE: Advertising"); + draw_centered(CONTENT_Y + 20, "Press to Disable"); + } +} + +static void render_advert(void) +{ + /* Check for recent "Sent!" feedback (show for 2 seconds) */ + uint32_t now = k_uptime_get_32(); + bool just_sent = (state.advert_sent_time > 0 && + (now - state.advert_sent_time) < 2000); + + draw_centered(CONTENT_Y, "Send Zero-Hop Advert"); + + if (just_sent) { + if (state.advert_was_flood) { + draw_centered(CONTENT_Y + 16, ">> Flood Sent! <<"); + } else { + draw_centered(CONTENT_Y + 16, ">>> Sent! <<<"); + } + } else { + draw_centered(CONTENT_Y + 16, "Press to Send"); + } + + draw_centered(CONTENT_Y + 34, "(Double Press: Flood)"); +} + +/* Format seconds into compact time string: "3m20s", "1h05m", "12s" */ +static void fmt_duration(char *buf, size_t len, uint32_t secs) +{ + if (secs >= 3600) { + snprintf(buf, len, "%uh%02um", secs / 3600, (secs % 3600) / 60); + } else if (secs >= 60) { + snprintf(buf, len, "%um%02us", secs / 60, secs % 60); + } else { + snprintf(buf, len, "%us", secs); + } +} + +static void render_gps(void) +{ + char buf[32]; + int y = CONTENT_Y; + + /* No GPS hardware on this board */ + if (!state.gps_available) { + mc_display_text(0, y, "GPS: not detected", false); + return; + } + + /* GPS enabled/disabled state */ + snprintf(buf, sizeof(buf), "GPS: %s", + state.gps_enabled ? "on" : "off"); + mc_display_text(0, y, buf, false); + y += LINE_H; + + if (!state.gps_enabled) { + draw_centered(y + 8, "Press to Enable"); + return; + } + + /* State-dependent display */ + if (state.gps_state == 2) { + /* ACQUIRING — actively searching for satellites */ + snprintf(buf, sizeof(buf), "Searching... sat:%u", + state.gps_satellites); + mc_display_text(0, y, buf, false); + y += LINE_H; + + /* Show last fix age if we have one */ + if (state.gps_last_fix_age_s != UINT32_MAX) { + char tbuf[12]; + + fmt_duration(tbuf, sizeof(tbuf), state.gps_last_fix_age_s); + snprintf(buf, sizeof(buf), "Last fix: %s ago", tbuf); + mc_display_text(0, y, buf, false); + } else { + mc_display_text(0, y, "No fix yet", false); + } + } else if (state.gps_state == 1) { + /* STANDBY — sleeping between fix cycles */ + if (state.gps_last_fix_age_s != UINT32_MAX) { + char tbuf[12]; + + fmt_duration(tbuf, sizeof(tbuf), state.gps_last_fix_age_s); + snprintf(buf, sizeof(buf), "Last fix: %s ago", tbuf); + mc_display_text(0, y, buf, false); + y += LINE_H; + } else { + mc_display_text(0, y, "No fix yet", false); + y += LINE_H; + } + + if (state.gps_next_search_s > 0) { + char tbuf[12]; + + fmt_duration(tbuf, sizeof(tbuf), state.gps_next_search_s); + snprintf(buf, sizeof(buf), "Next search: %s", tbuf); + mc_display_text(0, y, buf, false); + } + } else { + /* OFF — shouldn't reach here if gps_enabled is true */ + mc_display_text(0, y, "GPS off", false); + } +} + +static void render_buzzer(void) +{ + char buf[24]; + int y = CONTENT_Y; + + snprintf(buf, sizeof(buf), "Buzzer: %s", + state.buzzer_quiet ? "off" : "on"); + mc_display_text(0, y, buf, false); + y += LINE_H; + + draw_centered(y + 8, + state.buzzer_quiet ? "Press to Enable" : "Press to Disable"); +} + +static void render_sensors(void) +{ + char buf[24]; + int y = CONTENT_Y; + + /* Temperature */ + if (state.temperature_c10 != 0) { + snprintf(buf, sizeof(buf), "Temp: %d.%d C", + state.temperature_c10 / 10, + abs(state.temperature_c10 % 10)); + mc_display_text(0, y, buf, false); + y += LINE_H; + } + + /* Pressure */ + if (state.pressure_pa != 0) { + snprintf(buf, sizeof(buf), "Press: %u hPa", + (unsigned)(state.pressure_pa / 100)); + mc_display_text(0, y, buf, false); + y += LINE_H; + } + + /* Humidity */ + if (state.humidity_rh10 != 0) { + snprintf(buf, sizeof(buf), "Humid: %u.%u%%", + state.humidity_rh10 / 10, state.humidity_rh10 % 10); + mc_display_text(0, y, buf, false); + y += LINE_H; + } + + /* Light */ + if (state.light_lux != 0) { + snprintf(buf, sizeof(buf), "Light: %u lux", state.light_lux); + mc_display_text(0, y, buf, false); + y += LINE_H; + } + + /* Battery at bottom */ + snprintf(buf, sizeof(buf), "Batt: %u%% (%umV)", + state.battery_pct > 0 ? state.battery_pct + : calc_battery_pct(state.battery_mv), + state.battery_mv); + mc_display_text(0, y, buf, false); +} + +static void render_offgrid(void) +{ + /* Check if confirmation has expired (500ms) */ + if (state.offgrid_confirm_time != 0 && + (k_uptime_get_32() - state.offgrid_confirm_time) > 500) { + state.offgrid_confirm_time = 0; + } + + draw_centered(CONTENT_Y + 2, "Offgrid Mode"); + + char buf[24]; + + snprintf(buf, sizeof(buf), "Status: %s", + state.offgrid_enabled ? "ON" : "OFF"); + draw_centered(CONTENT_Y + 16, buf); + + if (state.offgrid_confirm_time != 0) { + draw_centered(CONTENT_Y + 34, "Press to confirm"); + } else { + draw_centered(CONTENT_Y + 34, + state.offgrid_enabled ? "Press to Disable" + : "Press to Enable"); + } +} + +static void render_dfu(void) +{ + /* Check if confirmation has expired (500ms) */ + if (state.dfu_confirm_time != 0 && + (k_uptime_get_32() - state.dfu_confirm_time) > 500) { + state.dfu_confirm_time = 0; + } + + draw_centered(CONTENT_Y + 10, "BLE DFU Update"); + if (state.dfu_confirm_time != 0) { + draw_centered(CONTENT_Y + 28, "Press to confirm"); + } else { + draw_centered(CONTENT_Y + 28, "Press to Run"); + } +} + +static void render_shutdown(void) +{ + /* Check if confirmation has expired (500ms) */ + if (state.shutdown_confirm_time != 0 && + (k_uptime_get_32() - state.shutdown_confirm_time) > 500) { + state.shutdown_confirm_time = 0; + } + + draw_centered(CONTENT_Y + 10, "Power Off"); + if (state.shutdown_confirm_time != 0) { + draw_centered(CONTENT_Y + 28, "Press to confirm"); + } else { + draw_centered(CONTENT_Y + 28, "Press to Run"); + } +} + +/* ========== Page render dispatch ========== */ + +typedef void (*page_render_fn)(void); + +static const page_render_fn renderers[] = { + [UI_PAGE_MESSAGES] = render_messages, + [UI_PAGE_RECENT] = render_recent, + [UI_PAGE_RADIO] = render_radio, + [UI_PAGE_BLUETOOTH] = render_bluetooth, + [UI_PAGE_ADVERT] = render_advert, + [UI_PAGE_GPS] = render_gps, + [UI_PAGE_BUZZER] = render_buzzer, + [UI_PAGE_SENSORS] = render_sensors, + [UI_PAGE_OFFGRID] = render_offgrid, + [UI_PAGE_DFU] = render_dfu, + [UI_PAGE_SHUTDOWN] = render_shutdown, +}; + +/* ========== Public API ========== */ + +struct ui_state *ui_pages_get_state(void) +{ + return &state; +} + +void ui_pages_render(void) +{ + mc_display_clear(); + render_top_bar(); + render_page_indicator(); + + if (state.current_page < UI_PAGE_COUNT && renderers[state.current_page]) { + renderers[state.current_page](); + } + + mc_display_finalize(); +} + +void ui_pages_render_splash(void) +{ + mc_display_clear(); + + /* ZephCore logo bitmap (128x13px) — center horizontally */ + int logo_w = 128; + int logo_h = 13; + int logo_x = (DISP_W >= logo_w) ? (DISP_W - logo_w) / 2 : 0; + int y = 3; + + mc_display_xbm(logo_x, y, zephcore_logo, logo_w, logo_h); + y += logo_h + LINE_H; + + /* "MeshCore on Zephyr" centered below logo */ + draw_centered(y, "MeshCore on Zephyr"); + y += LINE_H * 2; + + /* Build date centered below (format: "2026 Feb 15") */ +#ifdef FIRMWARE_BUILD_DATE + draw_centered(y, FIRMWARE_BUILD_DATE); +#endif + + mc_display_finalize(); +} + +void ui_pages_next(void) +{ + state.shutdown_confirm_time = 0; /* Reset confirmation on navigate */ + int page = (int)state.current_page + 1; + + if (page >= UI_PAGE_COUNT) { + page = 0; + } + state.current_page = (enum ui_page)page; +} + +void ui_pages_prev(void) +{ + state.shutdown_confirm_time = 0; /* Reset confirmation on navigate */ + int page = (int)state.current_page - 1; + + if (page < 0) { + page = UI_PAGE_COUNT - 1; + } + state.current_page = (enum ui_page)page; +} + +void ui_pages_set(enum ui_page page) +{ + if (page < UI_PAGE_COUNT) { + state.current_page = page; + } +} + +enum ui_page ui_pages_current(void) +{ + return state.current_page; +} + +void ui_pages_set_node_name(const char *name) +{ + if (name) { + /* Sanitize UTF-8 to Latin-1 (strips emojis, keeps accents) */ + utf8_to_latin1(state.node_name, name, sizeof(state.node_name)); + } +} + +void ui_pages_advert_sent(bool flood) +{ + state.advert_sent_time = k_uptime_get_32(); + state.advert_was_flood = flood; +} diff --git a/zephcore/helpers/ui/ui_pages.h b/zephcore/helpers/ui/ui_pages.h new file mode 100644 index 0000000..6770916 --- /dev/null +++ b/zephcore/helpers/ui/ui_pages.h @@ -0,0 +1,162 @@ +/* + * ZephCore - UI Page System + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Defines the multi-page UI matching Arduino's ui-new implementation. + * Each page has a render function that draws to the display. + */ + +#ifndef ZEPHCORE_UI_PAGES_H +#define ZEPHCORE_UI_PAGES_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ========== Page Definitions ========== */ +enum ui_page { + UI_PAGE_MESSAGES = 0, /* MSG count + connection status */ + UI_PAGE_RECENT, /* Recently heard contacts */ + UI_PAGE_RADIO, /* LoRa params (freq, SF, BW, CR, power, noise) */ + UI_PAGE_BLUETOOTH, /* BLE toggle */ + UI_PAGE_ADVERT, /* Send broadcast advert */ + UI_PAGE_GPS, /* GPS status / position */ + UI_PAGE_BUZZER, /* Buzzer mute toggle */ + UI_PAGE_SENSORS, /* Environment sensor data */ + UI_PAGE_OFFGRID, /* Offgrid mode (client repeat) toggle */ + UI_PAGE_DFU, /* BLE DFU bootloader entry */ + UI_PAGE_SHUTDOWN, /* Hibernate / power off */ + UI_PAGE_COUNT +}; + +/* ========== Page State (set by main app) ========== */ +struct ui_state { + /* Global / top bar */ + char node_name[24]; /* device node name for top bar */ + uint16_t battery_mv; + uint8_t battery_pct; + uint32_t rtc_epoch; /* Unix epoch from RTC (0 = not set) */ + + /* Messages page */ + uint16_t msg_count; + bool ble_connected; + char device_name[25]; + + /* Recently heard */ + struct { + char name[16]; + int16_t rssi; + uint32_t age_s; /* seconds since heard (recomputed each refresh) */ + } recent[4]; + uint8_t recent_count; + + /* Radio page */ + uint32_t lora_freq_hz; + uint8_t lora_sf; + uint16_t lora_bw_khz_x10; /* bandwidth in 0.1 kHz (e.g. 625 = 62.5 kHz) */ + uint8_t lora_cr; + int8_t lora_tx_power; + int16_t lora_noise_floor; + + /* Bluetooth page */ + bool ble_enabled; /* true = BLE active, false = serial mode */ + + /* GPS page */ + bool gps_available; /* true = GNSS hardware detected at boot */ + bool gps_enabled; /* GPS hardware on/off */ + bool gps_has_fix; + uint8_t gps_satellites; + int32_t gps_lat_mdeg; /* latitude in milli-degrees */ + int32_t gps_lon_mdeg; /* longitude in milli-degrees */ + int32_t gps_alt_mm; /* altitude in millimeters */ + uint8_t gps_state; /* 0=OFF, 1=STANDBY, 2=ACQUIRING */ + uint32_t gps_last_fix_age_s; /* seconds since last fix (UINT32_MAX=never) */ + uint32_t gps_next_search_s; /* seconds until next search (0=now/off) */ + + /* Buzzer page */ + bool buzzer_quiet; /* true = muted */ + + /* Sensors page */ + int16_t temperature_c10; /* temp in 0.1°C */ + uint32_t pressure_pa; /* pressure in Pa */ + uint16_t humidity_rh10; /* humidity in 0.1% RH */ + uint16_t light_lux; + + /* Current page */ + enum ui_page current_page; + + /* Offgrid mode (client repeat) */ + bool offgrid_enabled; /* true = forwarding packets */ + + /* Transient feedback (shown briefly after action) */ + uint32_t advert_sent_time; /* uptime ms when advert was sent (0=idle) */ + bool advert_was_flood; /* true if last advert was flood */ + uint32_t offgrid_confirm_time; /* uptime ms when offgrid toggle confirm started (0=idle) */ + uint32_t dfu_confirm_time; /* uptime ms when DFU confirm started (0=idle) */ + uint32_t shutdown_confirm_time; /* uptime ms when shutdown confirm started (0=idle) */ +}; + +/** + * Signal that an advert was just sent (shows feedback on advert page). + * @param flood true if flood advert, false if zero-hop + */ +void ui_pages_advert_sent(bool flood); + +/** + * Get the global UI state for updating from main app. + */ +struct ui_state *ui_pages_get_state(void); + +/** + * Render the current page to the display. + * Draws page indicator dots, then the page-specific content. + */ +void ui_pages_render(void); + +/** + * Render a splash screen. + */ +void ui_pages_render_splash(void); + +/** + * Navigate to next page (wrap around). + */ +void ui_pages_next(void); + +/** + * Navigate to previous page (wrap around). + */ +void ui_pages_prev(void); + +/** + * Set the current page directly. + */ +void ui_pages_set(enum ui_page page); + +/** + * Get the current page. + */ +enum ui_page ui_pages_current(void); + +/** + * Set the node name displayed in the top bar. + */ +void ui_pages_set_node_name(const char *name); + +/** + * Convert UTF-8 text to Latin-1 for display rendering. + * Passes ASCII unchanged, converts 2-byte Latin-1 (U+00A0-U+00FF), + * strips everything else (emojis, CJK). Trims leading/trailing spaces. + */ +void utf8_to_latin1(char *dst, const char *src, size_t dst_size); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHCORE_UI_PAGES_H */ diff --git a/zephcore/helpers/ui/ui_task.c b/zephcore/helpers/ui/ui_task.c new file mode 100644 index 0000000..40bf280 --- /dev/null +++ b/zephcore/helpers/ui/ui_task.c @@ -0,0 +1,1005 @@ +/* + * ZephCore - UI Task + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Integration layer that wires buttons, buzzer, and display together. + * All event-driven via Zephyr input subsystem + k_work. + * + * Input flow (after longpress + multi-tap filter chain): + * KEY_1 → action_page_next() (1 tap, 400ms delayed) + * KEY_B → action_flood_advert() (2 taps, 400ms delayed) + * KEY_D → action_buzzer_toggle() (3 taps, 400ms delayed) + * KEY_C → action_gps_toggle() (4 taps, immediate) + * KEY_POWER → action_deep_sleep() (long press ≥1s) + * KEY_RIGHT → action_page_next() (joystick, Wio Tracker) + * KEY_LEFT → action_page_prev() (joystick, Wio Tracker) + * KEY_ENTER → action_page_enter() (joystick center, Wio Tracker) + * + * Notification flow: + * LoRa RX → CompanionMesh → ui_notify(UI_EVENT_CONTACT_MSG) + * → buzzer_play(MELODY_MSG_CONTACT) + * → mc_display_on() + render + */ + +#include "ui_task.h" +#include "ui_pages.h" + +#ifdef CONFIG_ZEPHCORE_UI_BUZZER +#include "buzzer.h" +#endif + +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY +#include "display.h" +#endif + +#include + +#ifdef CONFIG_ZEPHCORE_EASTER_EGG_DOOM +#include "doom_game.h" +#endif +#include +#include +#include +#include + +#ifdef CONFIG_POWEROFF +#include +#endif + +#include + +/* GPS control (extern "C" in ZephyrSensorManager.h) */ +#include + +/* Mesh action wrappers (deferred to mesh event loop thread) */ +#include "ui_mesh_actions.h" + +#include +LOG_MODULE_REGISTER(ui_task, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); + +/* Tap-count feedback melodies. + * b=200, d=16 → each chirp ~75ms, rest ~75ms = clear separation. + * + * 2 taps (flood advert): chirp-chirp + * 3 taps (buzzer toggle): chirp×3 + high(ON) or low(OFF) + * 4 taps (GPS toggle): chirp×4 + high(ON) or low(OFF) + * + * ON tail: high E7 (~2637Hz) = "enabled" + * OFF tail: low G5 (~784Hz) = "disabled" */ +#define MELODY_BEEP_2 "b2:d=16,o=7,b=200:c,p,c" + +#define MELODY_BUZZER_ON "bon:d=16,o=7,b=200:c,p,c,p,c,p,p,8e" +#define MELODY_BUZZER_OFF "bof:d=16,o=7,b=200:c,p,c,p,c,p,p,8g5" + +#define MELODY_GPS_ON "gon:d=16,o=7,b=200:c,p,c,p,c,p,c,p,p,8e" +#define MELODY_GPS_OFF "gof:d=16,o=7,b=200:c,p,c,p,c,p,c,p,p,8g5" + +/* ========== LED Heartbeat ========== */ +/* Match Arduino: 4s cycle, 20ms pulse (normal) or 200ms (unread messages). + * Uses led0 or led1 alias — whichever exists in the board's DTS. + * Disabled when OLED display is present (display makes LED redundant). */ +#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) && !IS_ENABLED(CONFIG_ZEPHCORE_UI_DISPLAY) +#define LED_NODE DT_ALIAS(led0) +static const struct gpio_dt_spec heartbeat_led = + GPIO_DT_SPEC_GET(LED_NODE, gpios); +#define HAS_HEARTBEAT_LED 1 +#elif DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios) && !IS_ENABLED(CONFIG_ZEPHCORE_UI_DISPLAY) +#define LED_NODE DT_ALIAS(led1) +static const struct gpio_dt_spec heartbeat_led = + GPIO_DT_SPEC_GET(LED_NODE, gpios); +#define HAS_HEARTBEAT_LED 1 +#else +#define HAS_HEARTBEAT_LED 0 +#endif + +#define LED_CYCLE_MS 4000 /* Total heartbeat period */ +#define LED_ON_MS 20 /* Normal pulse width */ +#define LED_ON_MSG_MS 200 /* Pulse width when unread messages */ + +/* ========== Deep Sleep / System OFF ========== */ +/* On nRF52840, sys_poweroff() = System OFF (~1µA). + * Wake via reset button → full chip reset → boots fresh. */ + +/* When display is not available, maintain a local ui_state for setters + * (data can still be used for logging or future features). */ +#ifndef CONFIG_ZEPHCORE_UI_DISPLAY +static struct ui_state local_ui_state; +#endif + +/* ========== Constants ========== */ +#define SPLASH_DURATION_MS 3000 +#define RENDER_DEBOUNCE_MS 50 + +/* ========== State ========== */ +static bool ui_initialized; +static bool splash_active; + +/* ========== Doom Easter Egg Activation ========== */ +#ifdef CONFIG_ZEPHCORE_EASTER_EGG_DOOM +/* + * Activation sequence: double-click + single-click INPUT_KEY_ENTER on page 0. + * That's 3 presses total: press-press (double) then press (single). + * Deactivation: double-click INPUT_KEY_ENTER while Doom is running. + * + * State machine: + * IDLE → (1st press) → FIRST_PRESS + * FIRST_PRESS → (2nd press within 500ms) → DCLICK_DONE + * DCLICK_DONE → (3rd press within 500ms) → activate Doom + * Any state → (timeout) → IDLE + */ +enum doom_activate_state { + DOOM_ACT_IDLE, + DOOM_ACT_FIRST_PRESS, + DOOM_ACT_DCLICK_DONE, +}; + +static enum doom_activate_state doom_act_state; +static uint32_t doom_act_time; +#define DOOM_ACT_TIMEOUT_MS 500 +#endif + +static inline struct ui_state *get_state(void) +{ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + return ui_pages_get_state(); +#else + return &local_ui_state; +#endif +} + +/* Render work - debounced display update */ +static struct k_work_delayable render_work; +/* Splash timeout work */ +static struct k_work_delayable splash_work; +/* Deferred zero-hop advert — waits for possible double-press upgrade to flood */ +static struct k_work_delayable advert_defer_work; + +/* LED heartbeat only on boards with LED but no OLED (display makes it redundant) */ +#if HAS_HEARTBEAT_LED +/* LED heartbeat: two one-shot works form a self-rescheduling cycle. + * led_on_work turns LED on and schedules led_off_work after pulse width. + * led_off_work turns LED off and schedules led_on_work after remainder. */ +static struct k_work_delayable led_on_work; +static struct k_work_delayable led_off_work; +#endif + +/* ========== Work Handlers ========== */ + +#if HAS_HEARTBEAT_LED +static void led_off_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + gpio_pin_set_dt(&heartbeat_led, 0); + + /* Schedule next ON after remainder of cycle */ + struct ui_state *s = get_state(); + uint16_t on_ms = (s->msg_count > 0) ? LED_ON_MSG_MS : LED_ON_MS; + + k_work_reschedule(&led_on_work, K_MSEC(LED_CYCLE_MS - on_ms)); +} + +static void led_on_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + gpio_pin_set_dt(&heartbeat_led, 1); + + /* Schedule OFF after pulse width */ + struct ui_state *s = get_state(); + uint16_t on_ms = (s->msg_count > 0) ? LED_ON_MSG_MS : LED_ON_MS; + + k_work_reschedule(&led_off_work, K_MSEC(on_ms)); +} +#endif + +static void render_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + + if (!ui_initialized) { + return; + } + +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + if (mc_display_is_on() && !splash_active) { + ui_pages_render(); + } +#endif +} + +static void splash_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + splash_active = false; + +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + /* Transition from splash to home page */ + ui_pages_set(UI_PAGE_MESSAGES); + k_work_reschedule(&render_work, K_NO_WAIT); +#endif +} + +static void schedule_render(void); + +static void advert_defer_handler(struct k_work *work) +{ + ARG_UNUSED(work); + /* Timer expired without second press — send zero-hop */ + LOG_INF("zero-hop advert requested (deferred)"); +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + buzzer_play(MELODY_BEEP_2); +#endif + mesh_send_zerohop_advert(); +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + ui_pages_advert_sent(false); +#endif + schedule_render(); +} + +static void schedule_render(void) +{ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + k_work_reschedule(&render_work, K_MSEC(RENDER_DEBOUNCE_MS)); +#endif +} + +/* ========== Button Action Functions ========== */ +/* Each action checks capabilities internally — no #ifdef in the switch. */ + +static void action_page_next(void) +{ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + ui_pages_next(); +#endif + schedule_render(); +} + +static void action_page_prev(void) +{ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + ui_pages_prev(); +#endif + schedule_render(); +} + +/* Forward declarations for page-enter dispatch */ +static void action_flood_advert(void); +static void action_gps_toggle(void); +static void action_buzzer_toggle(void); +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY +static void action_ble_toggle(void); +static void action_enter_dfu(void); +#endif +static void action_deep_sleep(void); + +static void action_page_enter(void) +{ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + enum ui_page page = ui_pages_current(); + + LOG_INF("ENTER on page %d", page); + + switch (page) { + case UI_PAGE_BLUETOOTH: + /* Toggle BLE on/off */ + action_ble_toggle(); + break; + + case UI_PAGE_ADVERT: + /* Single press: defer 500ms then send zero-hop. + * Double press within 500ms: cancel deferred, send flood. */ + if (k_work_delayable_is_pending(&advert_defer_work)) { + /* Second press — cancel deferred zero-hop, send flood */ + k_work_cancel_delayable(&advert_defer_work); + action_flood_advert(); + } else { + /* First press — start deferred zero-hop */ + k_work_reschedule(&advert_defer_work, K_MSEC(500)); + } + break; + + case UI_PAGE_GPS: + /* Toggle GPS (same as quad-tap) */ + action_gps_toggle(); + break; + + case UI_PAGE_BUZZER: + /* Toggle buzzer mute (same as triple-tap) */ + action_buzzer_toggle(); + break; + + case UI_PAGE_OFFGRID: { + /* Double-press confirmation (500ms window) */ + struct ui_state *st_og = get_state(); + uint32_t now_og = k_uptime_get_32(); + + if (st_og->offgrid_confirm_time != 0 && + (now_og - st_og->offgrid_confirm_time) <= 500) { + /* Confirmed — toggle offgrid mode */ + bool new_state = !st_og->offgrid_enabled; + st_og->offgrid_enabled = new_state; + st_og->offgrid_confirm_time = 0; + mesh_set_offgrid_mode(new_state); + LOG_INF("offgrid mode %s (button)", new_state ? "on" : "off"); + } else { + /* First press — enter confirmation state */ + st_og->offgrid_confirm_time = now_og; + } + schedule_render(); + break; + } + + case UI_PAGE_DFU: { + /* Double-press confirmation (500ms window) */ + struct ui_state *st_dfu = get_state(); + uint32_t now_dfu = k_uptime_get_32(); + + if (st_dfu->dfu_confirm_time != 0 && + (now_dfu - st_dfu->dfu_confirm_time) <= 500) { + /* Confirmed — reboot into BLE DFU */ + action_enter_dfu(); + } else { + /* First press — enter confirmation state */ + st_dfu->dfu_confirm_time = now_dfu; + schedule_render(); + } + break; + } + + case UI_PAGE_SHUTDOWN: { + /* Double-press confirmation (500ms window) */ + struct ui_state *st = get_state(); + uint32_t now = k_uptime_get_32(); + + if (st->shutdown_confirm_time != 0 && + (now - st->shutdown_confirm_time) <= 500) { + /* Confirmed — shut down */ + action_deep_sleep(); + } else { + /* First press — enter confirmation state */ + st->shutdown_confirm_time = now; + schedule_render(); + } + break; + } + + case UI_PAGE_MESSAGES: +#ifdef CONFIG_ZEPHCORE_EASTER_EGG_DOOM + { + /* Doom activation: 3 presses — double-click + single-click. + * Press 1 → FIRST_PRESS + * Press 2 (within 500ms) → DCLICK_DONE + * Press 3 (within 500ms) → activate Doom */ + uint32_t now_doom = k_uptime_get_32(); + + switch (doom_act_state) { + case DOOM_ACT_IDLE: + doom_act_time = now_doom; + doom_act_state = DOOM_ACT_FIRST_PRESS; + break; + case DOOM_ACT_FIRST_PRESS: + if ((now_doom - doom_act_time) <= DOOM_ACT_TIMEOUT_MS) { + /* Double-click detected, wait for single */ + doom_act_time = now_doom; + doom_act_state = DOOM_ACT_DCLICK_DONE; + } else { + /* Timeout — restart as new first press */ + doom_act_time = now_doom; + doom_act_state = DOOM_ACT_FIRST_PRESS; + } + break; + case DOOM_ACT_DCLICK_DONE: + if ((now_doom - doom_act_time) <= DOOM_ACT_TIMEOUT_MS) { + /* Third press — activate Doom! */ + doom_act_state = DOOM_ACT_IDLE; + LOG_INF("Doom easter egg activated!"); + doom_game_start(); + } else { + /* Timeout — restart */ + doom_act_time = now_doom; + doom_act_state = DOOM_ACT_FIRST_PRESS; + } + break; + } + break; + } +#endif + /* fall through if doom not enabled */ + + default: + /* Other pages: no action on ENTER */ + break; + } +#endif +} + +static void action_flood_advert(void) +{ + LOG_INF("flood advert requested"); +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + buzzer_play(MELODY_BEEP_2); +#endif + mesh_send_flood_advert(); +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + ui_pages_advert_sent(true); +#endif + schedule_render(); +} + +static void action_buzzer_toggle(void) +{ +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + bool was_quiet = buzzer_is_quiet(); + + if (was_quiet) { + /* Unmuting: enable first, then play ascending confirmation */ + buzzer_set_quiet(false); + buzzer_play(MELODY_BUZZER_ON); + } else { + /* Muting: play descending confirmation while still enabled. + * Use deferred mute so the "off" melody plays out fully + * before the quiet flag suppresses future sounds. */ + buzzer_play(MELODY_BUZZER_OFF); + buzzer_set_quiet_deferred(true); + } + /* Persist mute state across reboots */ + mesh_set_buzzer_quiet(!was_quiet); + get_state()->buzzer_quiet = !was_quiet; + LOG_INF("buzzer %s", buzzer_is_quiet() ? "muted" : "unmuted"); +#endif + schedule_render(); +} + +static void action_gps_toggle(void) +{ + if (!gps_is_available()) { + LOG_WRN("GPS toggle ignored — no GPS hardware"); + return; + } + + bool now_enabled = !gps_is_enabled(); + + LOG_INF("GPS toggle → %s", now_enabled ? "on" : "off"); +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + buzzer_play(now_enabled ? MELODY_GPS_ON : MELODY_GPS_OFF); +#endif + /* Use persistent wrapper — same path as BLE CMD_SET_CUSTOM_VAR "gps" */ + mesh_gps_set_enabled(now_enabled); + schedule_render(); +} + +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY +static void action_ble_toggle(void) +{ + struct ui_state *s = get_state(); + bool now_enabled = !s->ble_enabled; + + LOG_INF("BLE toggle → %s", now_enabled ? "on" : "off"); + s->ble_enabled = now_enabled; + mesh_ble_set_enabled(now_enabled); + schedule_render(); +} + +static void action_enter_dfu(void) +{ + LOG_INF("entering BLE DFU bootloader"); + +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + buzzer_play(MELODY_SHUTDOWN); + while (buzzer_is_playing()) { + k_sleep(K_MSEC(50)); + } + buzzer_stop(); +#endif + + mc_display_clear(); + mc_display_text(16, 28, "BLE DFU...", false); + mc_display_finalize(); + k_sleep(K_MSEC(500)); + mc_display_off(); + + /* Delegate to board adapter — handles GPREGRET + reset for any platform */ + mesh_reboot_to_ota_dfu(); + CODE_UNREACHABLE; +} +#endif /* CONFIG_ZEPHCORE_UI_DISPLAY */ + +static void action_deep_sleep(void) +{ +#ifdef CONFIG_POWEROFF + LOG_INF("deep sleep: shutting down..."); + + /* 1. Stop LED heartbeat */ +#if HAS_HEARTBEAT_LED + k_work_cancel_delayable(&led_on_work); + k_work_cancel_delayable(&led_off_work); + gpio_pin_set_dt(&heartbeat_led, 0); +#endif + + /* 2. Play shutdown melody (blocking wait) */ +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + buzzer_play(MELODY_SHUTDOWN); + while (buzzer_is_playing()) { + k_sleep(K_MSEC(50)); + } + buzzer_stop(); +#endif + + /* 3. Turn display off */ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + mc_display_off(); +#endif + + /* 4. Drive power-hungry enable pins LOW. + * + * nRF52840 GPIO output latches persist across System OFF and reset. + * If GPS_EN is HIGH, the GPS module stays powered during "sleep." + * Drive known power-enable GPIOs LOW via Zephyr's safe GPIO API. + * + * DO NOT touch BLE (bt_conn_disconnect / bt_le_adv_stop) — that + * corrupts BLE controller state and prevents clean reboot. + * DO NOT blank all GPIOs — that bricked the device previously. */ + gps_power_off_for_shutdown(); + mesh_disable_power_regulators(); + + /* 5. Enter System OFF. + * Wake via reset button → full chip reset → clean boot. */ + LOG_INF("deep sleep: entering System OFF"); + sys_poweroff(); + CODE_UNREACHABLE; +#else + LOG_WRN("deep sleep: CONFIG_POWEROFF not enabled"); +#endif +} + +/* ========== Input Event Handler ========== */ + +static void ui_input_cb(struct input_event *evt, void *user_data) +{ + ARG_UNUSED(user_data); + + if (evt->type != INPUT_EV_KEY) { + return; + } + +#ifdef CONFIG_ZEPHCORE_EASTER_EGG_DOOM + /* When Doom is running, intercept ALL input (presses AND releases) */ + if (doom_game_is_running()) { + /* Double-click ENTER to exit: detect two presses within 500ms */ + if (evt->code == INPUT_KEY_ENTER && evt->value) { + static uint32_t doom_last_enter; + uint32_t now = k_uptime_get_32(); + + if (doom_last_enter != 0 && + (now - doom_last_enter) <= 500) { + /* Double-click — exit Doom */ + doom_last_enter = 0; + doom_game_stop(); + schedule_render(); + return; + } + doom_last_enter = now; + } + + /* Forward all key events (press + release) to Doom */ + doom_game_input(evt->code, evt->value); + return; + } +#endif + + /* Only handle key press events (value=1), not releases (value=0) */ + if (!evt->value) { + return; + } + + LOG_INF("UI input: code=%u value=%u", evt->code, evt->value); + +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + /* If display is off, wake it and consume the event */ + if (!mc_display_is_on()) { + mc_display_on(); + schedule_render(); + return; + } + + /* Reset auto-off timer on any button press */ + mc_display_reset_auto_off(); +#endif + + /* Dismiss splash screen on any button press */ + if (splash_active) { + k_work_cancel_delayable(&splash_work); + splash_active = false; +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + ui_pages_set(UI_PAGE_MESSAGES); +#endif + schedule_render(); + return; + } + + /* Map input key codes to UI actions. + * + * KEY_1: 1 tap (from multi-tap, 400ms delayed) + * KEY_B: 2 taps (from multi-tap, 400ms delayed) + * KEY_D: 3 taps (from multi-tap, 400ms delayed) + * KEY_C: 4 taps (from multi-tap, immediate — max count) + * KEY_POWER: long press (from longpress filter, ≥1s) + * KEY_RIGHT/LEFT/ENTER/UP/DOWN: joystick (Wio Tracker) + * + * NOTE: KEY_A (raw short-press from longpress filter) is NOT handled + * here. It feeds into the multi-tap filter which emits the tap codes. + * Since INPUT_CALLBACK_DEFINE(NULL) sees events from all devices, + * the raw KEY_A events fall through to default: break. + */ + switch (evt->code) { + /* ===== Multi-tap outputs ===== */ + case INPUT_KEY_1: + /* Single tap (400ms delayed): page next */ + action_page_next(); + break; + + case INPUT_KEY_B: + /* Double tap (400ms delayed): flood advert */ + action_flood_advert(); + break; + + case INPUT_KEY_D: + /* Triple tap (400ms delayed): toggle buzzer mute */ + action_buzzer_toggle(); + break; + + case INPUT_KEY_C: + /* Quadruple tap (immediate): toggle GPS */ + action_gps_toggle(); + break; + + /* ===== Longpress output ===== */ + case INPUT_KEY_POWER: + case INPUT_KEY_F: + /* Long press (≥1s): deep sleep */ + action_deep_sleep(); + break; + + /* ===== Joystick (Wio Tracker) ===== */ + case INPUT_KEY_RIGHT: + action_page_next(); + break; + + case INPUT_KEY_LEFT: + action_page_prev(); + break; + + case INPUT_KEY_ENTER: + action_page_enter(); + break; + + case INPUT_KEY_UP: + case INPUT_KEY_DOWN: + /* Joystick up/down - scroll within page (future) */ + break; + + default: + break; + } +} + +/* Register input callback for ALL devices (no specific device filter) */ +INPUT_CALLBACK_DEFINE(NULL, ui_input_cb, NULL); + +/* ========== Public API ========== */ + +int ui_init(void) +{ + int ret; + + k_work_init_delayable(&render_work, render_work_handler); + k_work_init_delayable(&splash_work, splash_work_handler); + k_work_init_delayable(&advert_defer_work, advert_defer_handler); + + /* Initialize buzzer (optional - may not be present) */ +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + ret = buzzer_init(); + if (ret == 0) { + LOG_INF("buzzer ready"); + } else if (ret == -ENODEV) { + LOG_INF("no buzzer hardware"); + } else { + LOG_WRN("buzzer init failed: %d", ret); + } +#endif + + /* Initialize display (optional - may not be present) */ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + ret = mc_display_init(); + if (ret == 0) { + LOG_INF("display ready"); + + /* Show splash screen */ + splash_active = true; + ui_pages_render_splash(); + + /* Schedule transition to home page */ + k_work_reschedule(&splash_work, K_MSEC(SPLASH_DURATION_MS)); + } else if (ret == -ENODEV) { + LOG_INF("no display hardware"); + } else { + LOG_WRN("display init failed: %d", ret); + } +#endif + + /* Initialize LED heartbeat — only on boards WITHOUT a display. + * If there's an OLED, the heartbeat LED is redundant and wastes power. */ +#if HAS_HEARTBEAT_LED + if (gpio_is_ready_dt(&heartbeat_led)) { + gpio_pin_configure_dt(&heartbeat_led, GPIO_OUTPUT_INACTIVE); + k_work_init_delayable(&led_on_work, led_on_work_handler); + k_work_init_delayable(&led_off_work, led_off_work_handler); + /* Start heartbeat cycle */ + k_work_reschedule(&led_on_work, K_NO_WAIT); + LOG_INF("LED heartbeat started"); + } +#endif + + /* NOTE: startup chime is NOT played here. It's played from main() + * after loadPrefs() so we can respect the persisted buzzer_quiet setting. + * See ui_play_startup_chime(). */ + + ui_initialized = true; + LOG_INF("UI initialized"); + + /* Suppress unused variable warning when both display and buzzer are disabled */ + (void)ret; + + return 0; +} + +void ui_play_startup_chime(void) +{ +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + if (!buzzer_is_quiet()) { + buzzer_play(MELODY_STARTUP); + } +#endif +} + +void ui_notify(enum ui_event event) +{ + if (!ui_initialized) { + return; + } + + bool is_msg_event = false; + + switch (event) { + case UI_EVENT_CONTACT_MSG: + is_msg_event = true; +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + /* Only buzz if no phone is connected to receive the message */ + if (!get_state()->ble_connected) { + buzzer_play(MELODY_MSG_CONTACT); + } +#endif + break; + + case UI_EVENT_CHANNEL_MSG: + is_msg_event = true; +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + if (!get_state()->ble_connected) { + buzzer_play(MELODY_MSG_CHANNEL); + } +#endif + break; + + case UI_EVENT_ROOM_MSG: + is_msg_event = true; +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + if (!get_state()->ble_connected) { + buzzer_play(MELODY_MSG_CHANNEL); + } +#endif + break; + + case UI_EVENT_ACK: +#ifdef CONFIG_ZEPHCORE_UI_BUZZER + buzzer_play(MELODY_ACK); +#endif + break; + + case UI_EVENT_BLE_CONNECTED: + ui_set_ble_status(true, NULL); + break; + + case UI_EVENT_BLE_DISCONNECTED: + ui_set_ble_status(false, NULL); + break; + + default: + break; + } + + /* Wake display on notifications — but skip for incoming messages + * when a phone is connected (phone handles its own notifications). */ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + if (!(is_msg_event && get_state()->ble_connected)) { + mc_display_on(); + schedule_render(); + } +#endif +} + +void ui_set_msg_count(uint16_t count) +{ + struct ui_state *s = get_state(); + + s->msg_count = count; + + if (ui_initialized) { + schedule_render(); + } +} + +void ui_set_ble_status(bool connected, const char *name) +{ + struct ui_state *s = get_state(); + + s->ble_connected = connected; + if (name) { + strncpy(s->device_name, name, sizeof(s->device_name) - 1); + s->device_name[sizeof(s->device_name) - 1] = '\0'; + } + + if (ui_initialized) { + schedule_render(); + } +} + +void ui_set_radio_params(uint32_t freq_hz, uint8_t sf, uint16_t bw_khz_x10, + uint8_t cr, int8_t tx_power, int16_t noise_floor) +{ + struct ui_state *s = get_state(); + + s->lora_freq_hz = freq_hz; + s->lora_sf = sf; + s->lora_bw_khz_x10 = bw_khz_x10; + s->lora_cr = cr; + s->lora_tx_power = tx_power; + s->lora_noise_floor = noise_floor; +} + +void ui_set_gps_data(bool has_fix, uint8_t sats, + int32_t lat_mdeg, int32_t lon_mdeg, int32_t alt_mm) +{ + struct ui_state *s = get_state(); + + s->gps_has_fix = has_fix; + s->gps_satellites = sats; + s->gps_lat_mdeg = lat_mdeg; + s->gps_lon_mdeg = lon_mdeg; + s->gps_alt_mm = alt_mm; +} + +void ui_set_battery(uint16_t mv, uint8_t pct) +{ + struct ui_state *s = get_state(); + + s->battery_mv = mv; + s->battery_pct = pct; +} + +void ui_set_clock(uint32_t epoch) +{ + struct ui_state *s = get_state(); + + s->rtc_epoch = epoch; +} + +void ui_add_recent(const char *name, int16_t rssi, uint32_t age_s) +{ + struct ui_state *s = get_state(); + + /* Shift entries down if full */ + if (s->recent_count >= 4) { + memmove(&s->recent[1], &s->recent[0], sizeof(s->recent[0]) * 3); + } else { + if (s->recent_count > 0) { + memmove(&s->recent[1], &s->recent[0], + sizeof(s->recent[0]) * s->recent_count); + } + s->recent_count++; + } + + /* Add new entry at top — sanitize UTF-8 to Latin-1 for display font */ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + utf8_to_latin1(s->recent[0].name, name, sizeof(s->recent[0].name)); +#else + strncpy(s->recent[0].name, name, sizeof(s->recent[0].name) - 1); + s->recent[0].name[sizeof(s->recent[0].name) - 1] = '\0'; +#endif + s->recent[0].rssi = rssi; + s->recent[0].age_s = age_s; +} + +void ui_clear_recent(void) +{ + struct ui_state *s = get_state(); + + s->recent_count = 0; + memset(s->recent, 0, sizeof(s->recent)); +} + +void ui_set_node_name(const char *name) +{ + struct ui_state *s = get_state(); + + if (name) { + strncpy(s->node_name, name, sizeof(s->node_name) - 1); + s->node_name[sizeof(s->node_name) - 1] = '\0'; + } +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + ui_pages_set_node_name(name); +#endif +} + +void ui_set_sensor_data(int16_t temp_c10, uint32_t pressure_pa, + uint16_t humidity_rh10, uint16_t light_lux) +{ + struct ui_state *s = get_state(); + + s->temperature_c10 = temp_c10; + s->pressure_pa = pressure_pa; + s->humidity_rh10 = humidity_rh10; + s->light_lux = light_lux; +} + +void ui_set_gps_available(bool available) +{ + struct ui_state *s = get_state(); + + s->gps_available = available; +} + +void ui_set_gps_enabled(bool enabled) +{ + struct ui_state *s = get_state(); + + s->gps_enabled = enabled; +} + +void ui_set_gps_state(uint8_t state, uint32_t last_fix_age_s, uint32_t next_search_s) +{ + struct ui_state *s = get_state(); + + s->gps_state = state; + s->gps_last_fix_age_s = last_fix_age_s; + s->gps_next_search_s = next_search_s; +} + +void ui_set_ble_enabled(bool enabled) +{ + struct ui_state *s = get_state(); + + s->ble_enabled = enabled; +} + +void ui_set_buzzer_quiet(bool quiet) +{ + struct ui_state *s = get_state(); + + s->buzzer_quiet = quiet; +} + +void ui_set_offgrid_mode(bool enabled) +{ + struct ui_state *s = get_state(); + + s->offgrid_enabled = enabled; +} + +void ui_refresh_display(void) +{ + if (!ui_initialized) { + return; + } + + schedule_render(); +} diff --git a/zephcore/helpers/ui/ui_task.h b/zephcore/helpers/ui/ui_task.h new file mode 100644 index 0000000..04fd7bc --- /dev/null +++ b/zephcore/helpers/ui/ui_task.h @@ -0,0 +1,165 @@ +/* + * ZephCore - UI Task + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Wires together buttons, buzzer, and display. + * Handles input events, page navigation, and notifications from mesh. + * + * All event-driven - no polling loops. + */ + +#ifndef ZEPHCORE_UI_TASK_H +#define ZEPHCORE_UI_TASK_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* UI notification events (from mesh/BLE layer) */ +enum ui_event { + UI_EVENT_NONE = 0, + UI_EVENT_CONTACT_MSG, /* New contact message received */ + UI_EVENT_CHANNEL_MSG, /* New channel message received */ + UI_EVENT_ROOM_MSG, /* New room message received */ + UI_EVENT_ACK, /* ACK received */ + UI_EVENT_BLE_CONNECTED, /* BLE peer connected */ + UI_EVENT_BLE_DISCONNECTED, /* BLE peer disconnected */ +}; + +/** + * Initialize the UI subsystem. + * Sets up display, buzzer, and input event callbacks. + * Safe to call even if no display/buzzer hardware is present. + * + * @return 0 on success + */ +int ui_init(void); + +/** + * Play the startup chime if buzzer is not muted. + * Call from main() AFTER loadPrefs() so the persisted buzzer_quiet + * setting is respected. No chime if user has muted the buzzer. + */ +void ui_play_startup_chime(void); + +/** + * Post a notification event to the UI. + * Triggers buzzer melody and/or display wake depending on event type. + * Safe to call from any thread context. + * + * @param event The UI event type + */ +void ui_notify(enum ui_event event); + +/** + * Update the message count shown on the messages page. + * + * @param count Current unread message count + */ +void ui_set_msg_count(uint16_t count); + +/** + * Update BLE connection status. + * + * @param connected true if BLE peer is connected + * @param name BLE device name (can be NULL) + */ +void ui_set_ble_status(bool connected, const char *name); + +/** + * Update radio parameters for display. + */ +void ui_set_radio_params(uint32_t freq_hz, uint8_t sf, uint16_t bw_khz_x10, + uint8_t cr, int8_t tx_power, int16_t noise_floor); + +/** + * Update GPS data for display. + */ +void ui_set_gps_data(bool has_fix, uint8_t sats, + int32_t lat_mdeg, int32_t lon_mdeg, int32_t alt_mm); + +/** + * Update battery data for display. + */ +void ui_set_battery(uint16_t mv, uint8_t pct); + +/** + * Update RTC clock for top bar display. + * @param epoch Unix timestamp (0 = time not set, hides clock) + */ +void ui_set_clock(uint32_t epoch); + +/** + * Record a recently heard contact for the "recent" page. + * + * @param name Contact name (truncated to 15 chars) + * @param rssi Signal strength in dBm + * @param age_s Seconds since contact was heard (computed from RTC) + */ +void ui_add_recent(const char *name, int16_t rssi, uint32_t age_s); + +/** + * Set the node name for the display top bar. + */ +void ui_set_node_name(const char *name); + +/** + * Clear and rebuild the recently heard contact list. + * Call ui_clear_recent() then ui_add_recent() for each entry. + */ +void ui_clear_recent(void); + +/** + * Update sensor data for display. + */ +void ui_set_sensor_data(int16_t temp_c10, uint32_t pressure_pa, + uint16_t humidity_rh10, uint16_t light_lux); + +/** + * Set whether GPS hardware was detected at boot. + * If false, GPS page shows "GPS: not detected". + */ +void ui_set_gps_available(bool available); + +/** + * Set GPS enabled state (for display page). + */ +void ui_set_gps_enabled(bool enabled); + +/** + * Update GPS state machine info for display. + * @param state 0=OFF, 1=STANDBY (sleeping), 2=ACQUIRING (searching) + * @param last_fix_age_s Seconds since last fix (UINT32_MAX = never) + * @param next_search_s Seconds until next search (0 = now or off) + */ +void ui_set_gps_state(uint8_t state, uint32_t last_fix_age_s, uint32_t next_search_s); + +/** + * Set BLE enabled state (for display page). + */ +void ui_set_ble_enabled(bool enabled); + +/** + * Set buzzer quiet state (for display page). + */ +void ui_set_buzzer_quiet(bool quiet); + +/** + * Set offgrid mode (client repeat) state for display page. + */ +void ui_set_offgrid_mode(bool enabled); + +/** + * Trigger a display refresh (for periodic updates from housekeeping). + */ +void ui_refresh_display(void); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHCORE_UI_TASK_H */ diff --git a/zephcore/include/mesh/Board.h b/zephcore/include/mesh/Board.h new file mode 100644 index 0000000..56a77c6 --- /dev/null +++ b/zephcore/include/mesh/Board.h @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore MainBoard interface - matches MeshCore.h + */ + +#pragma once + +#include +#include + +namespace mesh { + +#define BD_STARTUP_NORMAL 0 +#define BD_STARTUP_RX_PACKET 1 + +class MainBoard { +public: + virtual uint16_t getBattMilliVolts() = 0; + virtual float getMCUTemperature() { return NAN; } + virtual bool setAdcMultiplier(float multiplier) { (void)multiplier; return false; } + virtual float getAdcMultiplier() const { return 0.0f; } + virtual const char *getManufacturerName() const = 0; + virtual void onBeforeTransmit() {} + virtual void onAfterTransmit() {} + virtual void reboot() = 0; + virtual void powerOff() {} + virtual void sleep(uint32_t secs) { (void)secs; } + virtual uint32_t getGpio() { return 0; } + virtual void setGpio(uint32_t values) { (void)values; } + virtual uint8_t getStartupReason() const = 0; + virtual bool startOTAUpdate(const char *id, char reply[]) { (void)id; (void)reply; return false; } + + virtual bool isExternalPowered() { return false; } + virtual uint16_t getBootVoltage() { return 0; } + virtual uint32_t getResetReason() const { return 0; } + virtual const char *getResetReasonString(uint32_t reason) { (void)reason; return "Not available"; } + virtual uint8_t getShutdownReason() const { return 0; } + virtual const char *getShutdownReasonString(uint8_t reason) { (void)reason; return "Not available"; } +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/Clock.h b/zephcore/include/mesh/Clock.h new file mode 100644 index 0000000..8404924 --- /dev/null +++ b/zephcore/include/mesh/Clock.h @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore clock interfaces - matches Dispatcher.h / MeshCore.h + */ + +#pragma once + +#include + +namespace mesh { + +class MillisecondClock { +public: + virtual unsigned long getMillis() = 0; +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/Dispatcher.h b/zephcore/include/mesh/Dispatcher.h new file mode 100644 index 0000000..f947f36 --- /dev/null +++ b/zephcore/include/mesh/Dispatcher.h @@ -0,0 +1,141 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Dispatcher - packet queue and radio scheduling + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace mesh { + +/* EU ETSI EN 300 220 duty cycle tracker. + * Fixed 1-hour window, tracks cumulative TX airtime in ms. + * duty_pct=0 disables all tracking (zero overhead). */ +struct DutyCycleTracker { + uint32_t window_start; + uint32_t window_airtime_ms; + uint8_t duty_pct; /* 0=disabled, 1-99=percentage */ + + void init(uint8_t pct) { + window_start = 0; + window_airtime_ms = 0; + duty_pct = pct; + } + + void recordTx(uint32_t duration_ms, uint32_t now) { + if (duty_pct == 0) return; + if (now - window_start > 3600000UL) { + window_start = now; + window_airtime_ms = 0; + } + window_airtime_ms += duration_ms; + } + + bool isExceeded(uint32_t now) const { + if (duty_pct == 0) return false; + if (now - window_start > 3600000UL) return false; /* window expired */ + uint32_t budget_ms = (3600000UL / 100) * (uint32_t)duty_pct; + return window_airtime_ms >= budget_ms; + } + + uint32_t budgetMs() const { + if (duty_pct == 0) return 0; + return (3600000UL / 100) * (uint32_t)duty_pct; + } +}; + +class PacketManager { +public: + virtual Packet *allocNew() = 0; + virtual void free(Packet *packet) = 0; + virtual void queueOutbound(Packet *packet, uint8_t priority, uint32_t scheduled_for) = 0; + virtual Packet *getNextOutbound(uint32_t now) = 0; + virtual int getOutboundCount(uint32_t now) const = 0; + virtual int getFreeCount() const = 0; + virtual Packet *getOutboundByIdx(int i) = 0; + virtual Packet *removeOutboundByIdx(int i) = 0; + virtual void queueInbound(Packet *packet, uint32_t scheduled_for) = 0; + virtual Packet *getNextInbound(uint32_t now) = 0; +}; + +typedef uint32_t DispatcherAction; + +#define ACTION_RELEASE (0) +#define ACTION_MANUAL_HOLD (1) +#define ACTION_RETRANSMIT(pri) (((uint32_t)1 + (pri))<<24) +#define ACTION_RETRANSMIT_DELAYED(pri, _delay) ((((uint32_t)1 + (pri))<<24) | (_delay)) + +#define ERR_EVENT_FULL (1 << 0) +#define ERR_EVENT_CAD_TIMEOUT (1 << 1) +#define ERR_EVENT_STARTRX_TIMEOUT (1 << 2) + +class Dispatcher { + Packet *outbound; + uint32_t outbound_expiry, outbound_start, total_air_time, rx_air_time; + uint32_t next_tx_time; + uint32_t cad_busy_start; + DutyCycleTracker _duty_cycle; + uint32_t radio_nonrx_start; + uint32_t next_floor_calib_time, next_agc_reset_time; + bool prev_isrecv_mode; + uint32_t n_sent_flood, n_sent_direct; + uint32_t n_recv_flood, n_recv_direct; + + void processRecvPacket(Packet *pkt); + +protected: + Radio *_radio; + MillisecondClock *_ms; + PacketManager *_mgr; + uint16_t _err_flags; + + Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr); + virtual DispatcherAction onRecvPacket(Packet *pkt) = 0; + virtual void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { (void)snr; (void)rssi; (void)raw; (void)len; } + virtual void logRx(Packet *packet, int len, float score) { (void)packet; (void)len; (void)score; } + virtual void logTx(Packet *packet, int len) { (void)packet; (void)len; } + virtual void logTxFail(Packet *packet, int len) { (void)packet; (void)len; } + virtual const char *getLogDateTime() { return ""; } + virtual uint8_t getDutyCyclePercent() const; + static bool isAdminPacket(const Packet *pkt); + virtual int calcRxDelay(float score, uint32_t air_time) const; + virtual uint32_t getCADFailRetryDelay() const; + virtual uint32_t getCADFailMaxDuration() const; + virtual int getInterferenceThreshold() const { return 0; } + virtual int getAGCResetInterval() const { return 0; } + +public: + void begin(); + void loop(); + Packet *obtainNewPacket(); + void releasePacket(Packet *packet); + void sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis = 0); + + uint32_t getTotalAirTime() const { return total_air_time; } + uint32_t getReceiveAirTime() const { return rx_air_time; } + uint32_t getNumSentFlood() const { return n_sent_flood; } + uint32_t getNumSentDirect() const { return n_sent_direct; } + uint32_t getNumRecvFlood() const { return n_recv_flood; } + uint32_t getNumRecvDirect() const { return n_recv_direct; } + uint16_t getErrFlags() const { return _err_flags; } + void resetStats() { + n_sent_flood = n_sent_direct = 0; + n_recv_flood = n_recv_direct = 0; + _err_flags = 0; + } + bool millisHasNowPassed(uint32_t timestamp) const; + uint32_t futureMillis(int millis_from_now) const; + +private: + void checkRecv(); + void checkSend(); +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/Identity.h b/zephcore/include/mesh/Identity.h new file mode 100644 index 0000000..9b6c670 --- /dev/null +++ b/zephcore/include/mesh/Identity.h @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Identity - Ed25519 public identity + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace mesh { + +class Identity { +public: + uint8_t pub_key[PUB_KEY_SIZE]; + + Identity(); + Identity(const char *pub_hex); + Identity(const uint8_t *_pub) { memcpy(pub_key, _pub, PUB_KEY_SIZE); } + + int copyHashTo(uint8_t *dest) const { + memcpy(dest, pub_key, PATH_HASH_SIZE); + return PATH_HASH_SIZE; + } + bool isHashMatch(const uint8_t *hash) const { + return memcmp(hash, pub_key, PATH_HASH_SIZE) == 0; + } + bool isHashMatch(const uint8_t *hash, uint8_t len) const { + return memcmp(hash, pub_key, len) == 0; + } + bool verify(const uint8_t *sig, const uint8_t *message, int msg_len) const; + bool matches(const Identity &other) const { return memcmp(pub_key, other.pub_key, PUB_KEY_SIZE) == 0; } + bool matches(const uint8_t *other_pubkey) const { return memcmp(pub_key, other_pubkey, PUB_KEY_SIZE) == 0; } + bool readFrom(const uint8_t *src, size_t len); + bool writeTo(uint8_t *dest, size_t max_len) const; +}; + +class LocalIdentity : public Identity { + uint8_t prv_key[PRV_KEY_SIZE]; +public: + LocalIdentity(); + LocalIdentity(const char *prv_hex, const char *pub_hex); + LocalIdentity(RNG *rng); + + void sign(uint8_t *sig, const uint8_t *message, int msg_len) const; + void calcSharedSecret(uint8_t *secret, const Identity &other) const { calcSharedSecret(secret, other.pub_key); } + void calcSharedSecret(uint8_t *secret, const uint8_t *other_pub_key) const; + static bool validatePrivateKey(const uint8_t prv[64]); + + bool readFrom(const uint8_t *src, size_t len); + size_t writeTo(uint8_t *dest, size_t max_len) const; +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/LoRaConfig.h b/zephcore/include/mesh/LoRaConfig.h new file mode 100644 index 0000000..cc89eb9 --- /dev/null +++ b/zephcore/include/mesh/LoRaConfig.h @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore LoRa default parameters + */ + +#pragma once + +#include + +namespace mesh { + +struct LoRaConfig { + static constexpr uint32_t FREQ_HZ = 869618000; + static constexpr uint16_t BANDWIDTH = 62; /* BW_62_KHZ */ + static constexpr uint8_t SPREADING_FACTOR = 8; + static constexpr uint8_t CODING_RATE = 8; /* CR_4_8 */ + static constexpr uint16_t PREAMBLE_LEN = 16; + static constexpr int8_t TX_POWER_DBM = 22; +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/Mesh.h b/zephcore/include/mesh/Mesh.h new file mode 100644 index 0000000..e5ab5e0 --- /dev/null +++ b/zephcore/include/mesh/Mesh.h @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Mesh - routing protocol layer + */ + +#pragma once + +#include +#include + +namespace mesh { + +struct GroupChannel { + uint8_t hash[PATH_HASH_SIZE]; + uint8_t secret[PUB_KEY_SIZE]; +}; + +class MeshTables { +public: + virtual bool hasSeen(const Packet *packet) = 0; + virtual void clear(const Packet *packet) = 0; +}; + +class Mesh : public Dispatcher { + RNG *_rng; + RTCClock *_rtc; + MeshTables *_tables; + + void removeSelfFromPath(Packet *packet); + void routeDirectRecvAcks(Packet *packet, uint32_t delay_millis); + DispatcherAction forwardMultipartDirect(Packet *pkt); + +protected: + DispatcherAction onRecvPacket(Packet *pkt) override; + virtual uint32_t getCADFailRetryDelay() const override; + virtual DispatcherAction routeRecvPacket(Packet *packet); + virtual bool filterRecvFloodPacket(Packet *packet) { return false; } + virtual bool allowPacketForward(const Packet *packet); + virtual uint32_t getRetransmitDelay(const Packet *packet); + virtual uint32_t getDirectRetransmitDelay(const Packet *packet) { return 0; } + virtual uint8_t getExtraAckTransmitCount() const { return 0; } + virtual int searchPeersByHash(const uint8_t *hash) { (void)hash; return 0; } + virtual void getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { (void)dest_secret; (void)peer_idx; } + virtual void onPeerDataRecv(Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret, uint8_t *data, size_t len) { (void)packet; (void)type; (void)sender_idx; (void)secret; (void)data; (void)len; } + virtual void onTraceRecv(Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { (void)packet; (void)tag; (void)auth_code; (void)flags; (void)path_snrs; (void)path_hashes; (void)path_len; } + virtual bool onPeerPathRecv(Packet *packet, int sender_idx, const uint8_t *secret, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender_idx; (void)secret; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; return false; } + virtual void onAdvertRecv(Packet *packet, const Identity &id, uint32_t timestamp, const uint8_t *app_data, size_t app_data_len) { (void)packet; (void)id; (void)timestamp; (void)app_data; (void)app_data_len; } + virtual void onAnonDataRecv(Packet *packet, const uint8_t *secret, const Identity &sender, uint8_t *data, size_t len) { (void)packet; (void)secret; (void)sender; (void)data; (void)len; } + virtual void onPathRecv(Packet *packet, Identity &sender, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; } + virtual void onControlDataRecv(Packet *packet) { (void)packet; } + virtual void onRawDataRecv(Packet *packet) { (void)packet; } + virtual int searchChannelsByHash(const uint8_t *hash, GroupChannel channels[], int max_matches) { (void)hash; (void)channels; (void)max_matches; return 0; } + virtual void onGroupDataRecv(Packet *packet, uint8_t type, const GroupChannel &channel, uint8_t *data, size_t len) { (void)packet; (void)type; (void)channel; (void)data; (void)len; } + virtual void onAckRecv(Packet *packet, uint32_t ack_crc) { (void)packet; (void)ack_crc; } + +public: + Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables); + void begin(); + void loop(); + + LocalIdentity self_id; + + RNG *getRNG() const { return _rng; } + RTCClock *getRTCClock() const { return _rtc; } + MeshTables *getTables() const { return _tables; } + + Packet *createAdvert(const LocalIdentity &id, const uint8_t *app_data = nullptr, size_t app_data_len = 0); + Packet *createAck(uint32_t ack_crc); + Packet *createMultiAck(uint32_t ack_crc, uint8_t remaining); + Packet *createControlData(const uint8_t *data, size_t len); + Packet *createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t len); + Packet *createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len); + Packet *createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len); + Packet *createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len); + Packet *createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len); + Packet *createRawData(const uint8_t *data, size_t len); + Packet *createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0); + + void sendFlood(Packet *packet, uint32_t delay_millis = 0); + void sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0); + void sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis = 0); + void sendZeroHop(Packet *packet, uint32_t delay_millis = 0); + void sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0); +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/MeshCore.h b/zephcore/include/mesh/MeshCore.h new file mode 100644 index 0000000..e5b31b0 --- /dev/null +++ b/zephcore/include/mesh/MeshCore.h @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore constants and base types (Zephyr port) + */ + +#pragma once + +#include +#include + +#define MAX_HASH_SIZE 8 +#define PUB_KEY_SIZE 32 +#define PRV_KEY_SIZE 64 +#define SEED_SIZE 32 +#define SIGNATURE_SIZE 64 +#define MAX_ADVERT_DATA_SIZE 32 +#define CIPHER_KEY_SIZE 16 +#define CIPHER_BLOCK_SIZE 16 +#define CIPHER_MAC_SIZE 2 +#define PATH_HASH_SIZE 1 + +#define MAX_PACKET_PAYLOAD 184 +#define MAX_PATH_SIZE 64 +#define MAX_TRANS_UNIT 255 + +#define MESH_DEBUG_PRINT(...) +#define MESH_DEBUG_PRINTLN(...) + +namespace mesh { + +#define BD_STARTUP_NORMAL 0 +#define BD_STARTUP_RX_PACKET 1 + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/Packet.h b/zephcore/include/mesh/Packet.h new file mode 100644 index 0000000..45f11a7 --- /dev/null +++ b/zephcore/include/mesh/Packet.h @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Packet - fundamental transmission unit + */ + +#pragma once + +#include +#include +#include + +namespace mesh { + +#define PH_ROUTE_MASK 0x03 +#define PH_TYPE_SHIFT 2 +#define PH_TYPE_MASK 0x0F +#define PH_VER_SHIFT 6 +#define PH_VER_MASK 0x03 + +#define ROUTE_TYPE_TRANSPORT_FLOOD 0x00 +#define ROUTE_TYPE_FLOOD 0x01 +#define ROUTE_TYPE_DIRECT 0x02 +#define ROUTE_TYPE_TRANSPORT_DIRECT 0x03 + +#define PAYLOAD_TYPE_REQ 0x00 +#define PAYLOAD_TYPE_RESPONSE 0x01 +#define PAYLOAD_TYPE_TXT_MSG 0x02 +#define PAYLOAD_TYPE_ACK 0x03 +#define PAYLOAD_TYPE_ADVERT 0x04 +#define PAYLOAD_TYPE_GRP_TXT 0x05 +#define PAYLOAD_TYPE_GRP_DATA 0x06 +#define PAYLOAD_TYPE_ANON_REQ 0x07 +#define PAYLOAD_TYPE_PATH 0x08 +#define PAYLOAD_TYPE_TRACE 0x09 +#define PAYLOAD_TYPE_MULTIPART 0x0A +#define PAYLOAD_TYPE_CONTROL 0x0B +#define PAYLOAD_TYPE_RAW_CUSTOM 0x0F + +#define PAYLOAD_VER_1 0x00 +#define PAYLOAD_VER_2 0x01 +#define PAYLOAD_VER_3 0x02 +#define PAYLOAD_VER_4 0x03 + +class Packet { +public: + Packet(); + + uint8_t header; + uint16_t payload_len, path_len; + uint16_t transport_codes[2]; + uint8_t path[MAX_PATH_SIZE]; + uint8_t payload[MAX_PACKET_PAYLOAD]; + int8_t _snr; + + void calculatePacketHash(uint8_t *dest_hash) const; + uint8_t getRouteType() const { return header & PH_ROUTE_MASK; } + bool isRouteFlood() const { return getRouteType() == ROUTE_TYPE_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD; } + bool isRouteDirect() const { return getRouteType() == ROUTE_TYPE_DIRECT || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; } + bool hasTransportCodes() const { return getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; } + uint8_t getPayloadType() const { return (header >> PH_TYPE_SHIFT) & PH_TYPE_MASK; } + uint8_t getPayloadVer() const { return (header >> PH_VER_SHIFT) & PH_VER_MASK; } + void markDoNotRetransmit() { header = 0xFF; } + bool isMarkedDoNotRetransmit() const { return header == 0xFF; } + float getSNR() const { return ((float)_snr) / 4.0f; } + int getRawLength() const; + uint8_t writeTo(uint8_t dest[]) const; + bool readFrom(const uint8_t src[], uint8_t len); +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/RNG.h b/zephcore/include/mesh/RNG.h new file mode 100644 index 0000000..b8acd80 --- /dev/null +++ b/zephcore/include/mesh/RNG.h @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore RNG interface - matches Utils.h + */ + +#pragma once + +#include +#include + +namespace mesh { + +class RNG { +public: + virtual void random(uint8_t *dest, size_t sz) = 0; + + uint32_t nextInt(uint32_t _min, uint32_t _max); +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/RTC.h b/zephcore/include/mesh/RTC.h new file mode 100644 index 0000000..76c9992 --- /dev/null +++ b/zephcore/include/mesh/RTC.h @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore RTC interface - matches MeshCore.h + */ + +#pragma once + +#include + +namespace mesh { + +class RTCClock { + uint32_t last_unique; + +protected: + RTCClock() : last_unique(0) {} + +public: + virtual uint32_t getCurrentTime() = 0; + virtual void setCurrentTime(uint32_t time) = 0; + virtual void tick() {} + + uint32_t getCurrentTimeUnique() { + uint32_t t = getCurrentTime(); + if (t <= last_unique) { + return ++last_unique; + } + return last_unique = t; + } +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/Radio.h b/zephcore/include/mesh/Radio.h new file mode 100644 index 0000000..933c534 --- /dev/null +++ b/zephcore/include/mesh/Radio.h @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Radio interface - matches Dispatcher.h + */ + +#pragma once + +#include + +namespace mesh { + +class Radio { +public: + virtual void begin() {} + + virtual int recvRaw(uint8_t *bytes, int sz) = 0; + virtual uint32_t getEstAirtimeFor(int len_bytes) = 0; + virtual float packetScore(float snr, int packet_len) = 0; + virtual bool startSendRaw(const uint8_t *bytes, int len) = 0; + virtual bool isSendComplete() = 0; + virtual void onSendFinished() = 0; + + virtual int getNoiseFloor() const { return 0; } + virtual void triggerNoiseFloorCalibrate(int threshold) { (void)threshold; } + virtual void resetAGC() {} + + virtual bool isInRecvMode() const = 0; + virtual bool isReceiving() { return false; } + virtual float getLastRSSI() const { return 0; } + virtual float getLastSNR() const { return 0; } + + /* Packet statistics */ + virtual uint32_t getPacketsRecv() const { return 0; } + virtual uint32_t getPacketsSent() const { return 0; } + virtual uint32_t getPacketsRecvErrors() const { return 0; } +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/RadioIncludes.h b/zephcore/include/mesh/RadioIncludes.h new file mode 100644 index 0000000..172c78b --- /dev/null +++ b/zephcore/include/mesh/RadioIncludes.h @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore - Common radio includes for LoRa mesh roles + * + * Shared by companion, repeater, and any future role that uses the + * LoRa radio. Selects LR1110 or SX126x based on Kconfig/devicetree. + */ + +#ifndef ZEPHCORE_RADIO_INCLUDES_H +#define ZEPHCORE_RADIO_INCLUDES_H + +#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) || DT_NODE_HAS_STATUS(DT_ALIAS(lora0), okay) + +#include +#include +#include +#include +#include +#include +#include + +#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) +#include +#else +#include +#endif + +#define ZEPHCORE_LORA 1 + +#endif /* CONFIG_ZEPHCORE_RADIO_LR1110 || lora0 */ + +#endif /* ZEPHCORE_RADIO_INCLUDES_H */ diff --git a/zephcore/include/mesh/SimpleMeshTables.h b/zephcore/include/mesh/SimpleMeshTables.h new file mode 100644 index 0000000..00b246b --- /dev/null +++ b/zephcore/include/mesh/SimpleMeshTables.h @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SimpleMeshTables - hash-based deduplication + */ + +#pragma once + +#include +#include + +namespace mesh { + +#define MAX_PACKET_HASHES 64 +#define MAX_PACKET_ACKS 32 + +class SimpleMeshTables : public MeshTables { + uint8_t _hashes[MAX_PACKET_HASHES * MAX_HASH_SIZE]; + int _next_idx; + uint32_t _acks[MAX_PACKET_ACKS]; + int _next_ack_idx; + +public: + SimpleMeshTables() { + memset(_hashes, 0, sizeof(_hashes)); + _next_idx = 0; + memset(_acks, 0, sizeof(_acks)); + _next_ack_idx = 0; + } + + bool hasSeen(const Packet *packet) override { + if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack; + memcpy(&ack, packet->payload, 4); + for (int i = 0; i < MAX_PACKET_ACKS; i++) { + if (ack == _acks[i]) return true; + } + _acks[_next_ack_idx] = ack; + _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; + return false; + } + + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + const uint8_t *sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) return true; + } + memcpy(&_hashes[_next_idx * MAX_HASH_SIZE], hash, MAX_HASH_SIZE); + _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; + return false; + } + + void clear(const Packet *packet) override { + if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack; + memcpy(&ack, packet->payload, 4); + for (int i = 0; i < MAX_PACKET_ACKS; i++) { + if (ack == _acks[i]) { + _acks[i] = 0; + break; + } + } + } else { + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + uint8_t *sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + memset(sp, 0, MAX_HASH_SIZE); + break; + } + } + } + } +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/StaticPoolPacketManager.h b/zephcore/include/mesh/StaticPoolPacketManager.h new file mode 100644 index 0000000..27050e6 --- /dev/null +++ b/zephcore/include/mesh/StaticPoolPacketManager.h @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Static pool PacketManager + */ + +#pragma once + +#include + +namespace mesh { + +class StaticPoolPacketManager : public PacketManager { +public: + Packet *allocNew() override; + void free(Packet *packet) override; + void queueOutbound(Packet *packet, uint8_t priority, uint32_t scheduled_for) override; + Packet *getNextOutbound(uint32_t now) override; + int getOutboundCount(uint32_t now) const override; + int getFreeCount() const override; + Packet *getOutboundByIdx(int i) override; + Packet *removeOutboundByIdx(int i) override; + void queueInbound(Packet *packet, uint32_t scheduled_for) override; + Packet *getNextInbound(uint32_t now) override; +}; + +} /* namespace mesh */ diff --git a/zephcore/include/mesh/Utils.h b/zephcore/include/mesh/Utils.h new file mode 100644 index 0000000..b7d0daf --- /dev/null +++ b/zephcore/include/mesh/Utils.h @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Utils - crypto and helpers (Zephyr port) + */ + +#pragma once + +#include +#include +#include + +namespace mesh { + +class Utils { +public: + static void sha256(uint8_t *hash, size_t hash_len, const uint8_t *msg, int msg_len); + static void sha256(uint8_t *hash, size_t hash_len, const uint8_t *frag1, int frag1_len, const uint8_t *frag2, int frag2_len); + static int encrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len); + static int decrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len); + static int encryptThenMAC(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len); + static int MACThenDecrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len); + static void toHex(char *dest, const uint8_t *src, size_t len); + static bool fromHex(uint8_t *dest, int dest_size, const char *src_hex); + static bool isHexChar(char c); + static int parseTextParts(char *text, const char *parts[], int max_num, char separator = ','); +}; + +} /* namespace mesh */ diff --git a/zephcore/lib/.gitkeep b/zephcore/lib/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/zephcore/lib/.gitkeep @@ -0,0 +1 @@ + diff --git a/zephcore/lib/ed25519/CMakeLists.txt b/zephcore/lib/ed25519/CMakeLists.txt new file mode 100644 index 0000000..6ad441c --- /dev/null +++ b/zephcore/lib/ed25519/CMakeLists.txt @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# Vendored ed25519 (orlp/ed25519) - no seed.c (ED25519_NO_SEED) +# Note: CMake warning about "not treated as a Zephyr library" is harmless - library still works. + +zephyr_library_named(ed25519) +zephyr_library_sources( + add_scalar.c + fe.c + ge.c + key_exchange.c + keypair.c + sc.c + sha512.c + sign.c + verify.c +) +zephyr_library_compile_definitions(ED25519_NO_SEED=1) +zephyr_library_include_directories(.) diff --git a/zephcore/lib/ed25519/add_scalar.c b/zephcore/lib/ed25519/add_scalar.c new file mode 100644 index 0000000..77f1fd8 --- /dev/null +++ b/zephcore/lib/ed25519/add_scalar.c @@ -0,0 +1,69 @@ +#include "ed_25519.h" +#include "ge.h" +#include "sc.h" +#include "sha512.h" + + +/* see http://crypto.stackexchange.com/a/6215/4697 */ +void ed25519_add_scalar(unsigned char *public_key, unsigned char *private_key, const unsigned char *scalar) { + const unsigned char SC_1[32] = {1}; /* scalar with value 1 */ + + unsigned char n[32]; + ge_p3 nB; + ge_p1p1 A_p1p1; + ge_p3 A; + ge_p3 public_key_unpacked; + ge_cached T; + + sha512_context hash; + unsigned char hashbuf[64]; + + int i; + + /* copy the scalar and clear highest bit */ + for (i = 0; i < 31; ++i) { + n[i] = scalar[i]; + } + n[31] = scalar[31] & 127; + + /* private key: a = n + t */ + if (private_key) { + sc_muladd(private_key, SC_1, n, private_key); + + // https://github.com/orlp/ed25519/issues/3 + sha512_init(&hash); + sha512_update(&hash, private_key + 32, 32); + sha512_update(&hash, scalar, 32); + sha512_final(&hash, hashbuf); + for (i = 0; i < 32; ++i) { + private_key[32 + i] = hashbuf[i]; + } + } + + /* public key: A = nB + T */ + if (public_key) { + /* if we know the private key we don't need a point addition, which is faster */ + /* using a "timing attack" you could find out wether or not we know the private + key, but this information seems rather useless - if this is important pass + public_key and private_key seperately in 2 function calls */ + if (private_key) { + ge_scalarmult_base(&A, private_key); + } else { + /* unpack public key into T */ + ge_frombytes_negate_vartime(&public_key_unpacked, public_key); + fe_neg(public_key_unpacked.X, public_key_unpacked.X); /* undo negate */ + fe_neg(public_key_unpacked.T, public_key_unpacked.T); /* undo negate */ + ge_p3_to_cached(&T, &public_key_unpacked); + + /* calculate n*B */ + ge_scalarmult_base(&nB, n); + + /* A = n*B + T */ + ge_add(&A_p1p1, &nB, &T); + ge_p1p1_to_p3(&A, &A_p1p1); + } + + /* pack public key */ + ge_p3_tobytes(public_key, &A); + } +} diff --git a/zephcore/lib/ed25519/ed_25519.h b/zephcore/lib/ed25519/ed_25519.h new file mode 100644 index 0000000..5ac015e --- /dev/null +++ b/zephcore/lib/ed25519/ed_25519.h @@ -0,0 +1,41 @@ +#ifndef ED25519_H +#define ED25519_H + +// Nightcracker's Ed25519 - https://github.com/orlp/ed25519 + +#include + +#if defined(_WIN32) + #if defined(ED25519_BUILD_DLL) + #define ED25519_DECLSPEC __declspec(dllexport) + #elif defined(ED25519_DLL) + #define ED25519_DECLSPEC __declspec(dllimport) + #else + #define ED25519_DECLSPEC + #endif +#else + #define ED25519_DECLSPEC +#endif + + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef ED25519_NO_SEED +int ED25519_DECLSPEC ed25519_create_seed(unsigned char *seed); +#endif + +void ED25519_DECLSPEC ed25519_create_keypair(unsigned char *public_key, unsigned char *private_key, const unsigned char *seed); +void ED25519_DECLSPEC ed25519_derive_pub(unsigned char *public_key, const unsigned char *private_key); +void ED25519_DECLSPEC ed25519_sign(unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key, const unsigned char *private_key); +int ED25519_DECLSPEC ed25519_verify(const unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key); +void ED25519_DECLSPEC ed25519_add_scalar(unsigned char *public_key, unsigned char *private_key, const unsigned char *scalar); +void ED25519_DECLSPEC ed25519_key_exchange(unsigned char *shared_secret, const unsigned char *public_key, const unsigned char *private_key); + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/zephcore/lib/ed25519/fe.c b/zephcore/lib/ed25519/fe.c new file mode 100644 index 0000000..2105eb7 --- /dev/null +++ b/zephcore/lib/ed25519/fe.c @@ -0,0 +1,1491 @@ +#include "fixedint.h" +#include "fe.h" + + +/* + helper functions +*/ +static uint64_t load_3(const unsigned char *in) { + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + + return result; +} + +static uint64_t load_4(const unsigned char *in) { + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + result |= ((uint64_t) in[3]) << 24; + + return result; +} + + + +/* + h = 0 +*/ + +void fe_0(fe h) { + h[0] = 0; + h[1] = 0; + h[2] = 0; + h[3] = 0; + h[4] = 0; + h[5] = 0; + h[6] = 0; + h[7] = 0; + h[8] = 0; + h[9] = 0; +} + + + +/* + h = 1 +*/ + +void fe_1(fe h) { + h[0] = 1; + h[1] = 0; + h[2] = 0; + h[3] = 0; + h[4] = 0; + h[5] = 0; + h[6] = 0; + h[7] = 0; + h[8] = 0; + h[9] = 0; +} + + + +/* + h = f + g + Can overlap h with f or g. + + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + + Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +*/ + +void fe_add(fe h, const fe f, const fe g) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + int32_t h0 = f0 + g0; + int32_t h1 = f1 + g1; + int32_t h2 = f2 + g2; + int32_t h3 = f3 + g3; + int32_t h4 = f4 + g4; + int32_t h5 = f5 + g5; + int32_t h6 = f6 + g6; + int32_t h7 = f7 + g7; + int32_t h8 = f8 + g8; + int32_t h9 = f9 + g9; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + + + +/* + Replace (f,g) with (g,g) if b == 1; + replace (f,g) with (f,g) if b == 0. + + Preconditions: b in {0,1}. +*/ + +void fe_cmov(fe f, const fe g, unsigned int b) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + int32_t x0 = f0 ^ g0; + int32_t x1 = f1 ^ g1; + int32_t x2 = f2 ^ g2; + int32_t x3 = f3 ^ g3; + int32_t x4 = f4 ^ g4; + int32_t x5 = f5 ^ g5; + int32_t x6 = f6 ^ g6; + int32_t x7 = f7 ^ g7; + int32_t x8 = f8 ^ g8; + int32_t x9 = f9 ^ g9; + + b = (unsigned int) (- (int) b); /* silence warning */ + x0 &= b; + x1 &= b; + x2 &= b; + x3 &= b; + x4 &= b; + x5 &= b; + x6 &= b; + x7 &= b; + x8 &= b; + x9 &= b; + + f[0] = f0 ^ x0; + f[1] = f1 ^ x1; + f[2] = f2 ^ x2; + f[3] = f3 ^ x3; + f[4] = f4 ^ x4; + f[5] = f5 ^ x5; + f[6] = f6 ^ x6; + f[7] = f7 ^ x7; + f[8] = f8 ^ x8; + f[9] = f9 ^ x9; +} + +/* + Replace (f,g) with (g,f) if b == 1; + replace (f,g) with (f,g) if b == 0. + + Preconditions: b in {0,1}. +*/ + +void fe_cswap(fe f,fe g,unsigned int b) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + int32_t x0 = f0 ^ g0; + int32_t x1 = f1 ^ g1; + int32_t x2 = f2 ^ g2; + int32_t x3 = f3 ^ g3; + int32_t x4 = f4 ^ g4; + int32_t x5 = f5 ^ g5; + int32_t x6 = f6 ^ g6; + int32_t x7 = f7 ^ g7; + int32_t x8 = f8 ^ g8; + int32_t x9 = f9 ^ g9; + b = (unsigned int) (- (int) b); /* silence warning */ + x0 &= b; + x1 &= b; + x2 &= b; + x3 &= b; + x4 &= b; + x5 &= b; + x6 &= b; + x7 &= b; + x8 &= b; + x9 &= b; + f[0] = f0 ^ x0; + f[1] = f1 ^ x1; + f[2] = f2 ^ x2; + f[3] = f3 ^ x3; + f[4] = f4 ^ x4; + f[5] = f5 ^ x5; + f[6] = f6 ^ x6; + f[7] = f7 ^ x7; + f[8] = f8 ^ x8; + f[9] = f9 ^ x9; + g[0] = g0 ^ x0; + g[1] = g1 ^ x1; + g[2] = g2 ^ x2; + g[3] = g3 ^ x3; + g[4] = g4 ^ x4; + g[5] = g5 ^ x5; + g[6] = g6 ^ x6; + g[7] = g7 ^ x7; + g[8] = g8 ^ x8; + g[9] = g9 ^ x9; +} + + + +/* + h = f +*/ + +void fe_copy(fe h, const fe f) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + h[0] = f0; + h[1] = f1; + h[2] = f2; + h[3] = f3; + h[4] = f4; + h[5] = f5; + h[6] = f6; + h[7] = f7; + h[8] = f8; + h[9] = f9; +} + + + +/* + Ignores top bit of h. +*/ + +void fe_frombytes(fe h, const unsigned char *s) { + int64_t h0 = load_4(s); + int64_t h1 = load_3(s + 4) << 6; + int64_t h2 = load_3(s + 7) << 5; + int64_t h3 = load_3(s + 10) << 3; + int64_t h4 = load_3(s + 13) << 2; + int64_t h5 = load_4(s + 16); + int64_t h6 = load_3(s + 20) << 7; + int64_t h7 = load_3(s + 23) << 5; + int64_t h8 = load_3(s + 26) << 4; + int64_t h9 = (load_3(s + 29) & 8388607) << 2; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + carry9 = (h9 + (int64_t) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + carry1 = (h1 + (int64_t) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry3 = (h3 + (int64_t) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry5 = (h5 + (int64_t) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry7 = (h7 + (int64_t) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry2 = (h2 + (int64_t) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry6 = (h6 + (int64_t) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry8 = (h8 + (int64_t) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + + + +void fe_invert(fe out, const fe z) { + fe t0; + fe t1; + fe t2; + fe t3; + int i; + + fe_sq(t0, z); + + for (i = 1; i < 1; ++i) { + fe_sq(t0, t0); + } + + fe_sq(t1, t0); + + for (i = 1; i < 2; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t1, z, t1); + fe_mul(t0, t0, t1); + fe_sq(t2, t0); + + for (i = 1; i < 1; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t1, t2); + fe_sq(t2, t1); + + for (i = 1; i < 5; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t2, t1); + fe_sq(t2, t1); + + for (i = 1; i < 10; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t2, t2, t1); + fe_sq(t3, t2); + + for (i = 1; i < 20; ++i) { + fe_sq(t3, t3); + } + + fe_mul(t2, t3, t2); + fe_sq(t2, t2); + + for (i = 1; i < 10; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t2, t1); + fe_sq(t2, t1); + + for (i = 1; i < 50; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t2, t2, t1); + fe_sq(t3, t2); + + for (i = 1; i < 100; ++i) { + fe_sq(t3, t3); + } + + fe_mul(t2, t3, t2); + fe_sq(t2, t2); + + for (i = 1; i < 50; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t2, t1); + fe_sq(t1, t1); + + for (i = 1; i < 5; ++i) { + fe_sq(t1, t1); + } + + fe_mul(out, t1, t0); +} + + + +/* + return 1 if f is in {1,3,5,...,q-2} + return 0 if f is in {0,2,4,...,q-1} + + Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +*/ + +int fe_isnegative(const fe f) { + unsigned char s[32]; + + fe_tobytes(s, f); + + return s[0] & 1; +} + + + +/* + return 1 if f == 0 + return 0 if f != 0 + + Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +*/ + +int fe_isnonzero(const fe f) { + unsigned char s[32]; + unsigned char r; + + fe_tobytes(s, f); + + r = s[0]; + #define F(i) r |= s[i] + F(1); + F(2); + F(3); + F(4); + F(5); + F(6); + F(7); + F(8); + F(9); + F(10); + F(11); + F(12); + F(13); + F(14); + F(15); + F(16); + F(17); + F(18); + F(19); + F(20); + F(21); + F(22); + F(23); + F(24); + F(25); + F(26); + F(27); + F(28); + F(29); + F(30); + F(31); + #undef F + + return r != 0; +} + + + +/* + h = f * g + Can overlap h with f or g. + + Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + |g| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + + Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. + */ + + /* + Notes on implementation strategy: + + Using schoolbook multiplication. + Karatsuba would save a little in some cost models. + + Most multiplications by 2 and 19 are 32-bit precomputations; + cheaper than 64-bit postcomputations. + + There is one remaining multiplication by 19 in the carry chain; + one *19 precomputation can be merged into this, + but the resulting data flow is considerably less clean. + + There are 12 carries below. + 10 of them are 2-way parallelizable and vectorizable. + Can get away with 11 carries, but then data flow is much deeper. + + With tighter constraints on inputs can squeeze carries into int32. +*/ + +void fe_mul(fe h, const fe f, const fe g) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + int32_t g1_19 = 19 * g1; /* 1.959375*2^29 */ + int32_t g2_19 = 19 * g2; /* 1.959375*2^30; still ok */ + int32_t g3_19 = 19 * g3; + int32_t g4_19 = 19 * g4; + int32_t g5_19 = 19 * g5; + int32_t g6_19 = 19 * g6; + int32_t g7_19 = 19 * g7; + int32_t g8_19 = 19 * g8; + int32_t g9_19 = 19 * g9; + int32_t f1_2 = 2 * f1; + int32_t f3_2 = 2 * f3; + int32_t f5_2 = 2 * f5; + int32_t f7_2 = 2 * f7; + int32_t f9_2 = 2 * f9; + int64_t f0g0 = f0 * (int64_t) g0; + int64_t f0g1 = f0 * (int64_t) g1; + int64_t f0g2 = f0 * (int64_t) g2; + int64_t f0g3 = f0 * (int64_t) g3; + int64_t f0g4 = f0 * (int64_t) g4; + int64_t f0g5 = f0 * (int64_t) g5; + int64_t f0g6 = f0 * (int64_t) g6; + int64_t f0g7 = f0 * (int64_t) g7; + int64_t f0g8 = f0 * (int64_t) g8; + int64_t f0g9 = f0 * (int64_t) g9; + int64_t f1g0 = f1 * (int64_t) g0; + int64_t f1g1_2 = f1_2 * (int64_t) g1; + int64_t f1g2 = f1 * (int64_t) g2; + int64_t f1g3_2 = f1_2 * (int64_t) g3; + int64_t f1g4 = f1 * (int64_t) g4; + int64_t f1g5_2 = f1_2 * (int64_t) g5; + int64_t f1g6 = f1 * (int64_t) g6; + int64_t f1g7_2 = f1_2 * (int64_t) g7; + int64_t f1g8 = f1 * (int64_t) g8; + int64_t f1g9_38 = f1_2 * (int64_t) g9_19; + int64_t f2g0 = f2 * (int64_t) g0; + int64_t f2g1 = f2 * (int64_t) g1; + int64_t f2g2 = f2 * (int64_t) g2; + int64_t f2g3 = f2 * (int64_t) g3; + int64_t f2g4 = f2 * (int64_t) g4; + int64_t f2g5 = f2 * (int64_t) g5; + int64_t f2g6 = f2 * (int64_t) g6; + int64_t f2g7 = f2 * (int64_t) g7; + int64_t f2g8_19 = f2 * (int64_t) g8_19; + int64_t f2g9_19 = f2 * (int64_t) g9_19; + int64_t f3g0 = f3 * (int64_t) g0; + int64_t f3g1_2 = f3_2 * (int64_t) g1; + int64_t f3g2 = f3 * (int64_t) g2; + int64_t f3g3_2 = f3_2 * (int64_t) g3; + int64_t f3g4 = f3 * (int64_t) g4; + int64_t f3g5_2 = f3_2 * (int64_t) g5; + int64_t f3g6 = f3 * (int64_t) g6; + int64_t f3g7_38 = f3_2 * (int64_t) g7_19; + int64_t f3g8_19 = f3 * (int64_t) g8_19; + int64_t f3g9_38 = f3_2 * (int64_t) g9_19; + int64_t f4g0 = f4 * (int64_t) g0; + int64_t f4g1 = f4 * (int64_t) g1; + int64_t f4g2 = f4 * (int64_t) g2; + int64_t f4g3 = f4 * (int64_t) g3; + int64_t f4g4 = f4 * (int64_t) g4; + int64_t f4g5 = f4 * (int64_t) g5; + int64_t f4g6_19 = f4 * (int64_t) g6_19; + int64_t f4g7_19 = f4 * (int64_t) g7_19; + int64_t f4g8_19 = f4 * (int64_t) g8_19; + int64_t f4g9_19 = f4 * (int64_t) g9_19; + int64_t f5g0 = f5 * (int64_t) g0; + int64_t f5g1_2 = f5_2 * (int64_t) g1; + int64_t f5g2 = f5 * (int64_t) g2; + int64_t f5g3_2 = f5_2 * (int64_t) g3; + int64_t f5g4 = f5 * (int64_t) g4; + int64_t f5g5_38 = f5_2 * (int64_t) g5_19; + int64_t f5g6_19 = f5 * (int64_t) g6_19; + int64_t f5g7_38 = f5_2 * (int64_t) g7_19; + int64_t f5g8_19 = f5 * (int64_t) g8_19; + int64_t f5g9_38 = f5_2 * (int64_t) g9_19; + int64_t f6g0 = f6 * (int64_t) g0; + int64_t f6g1 = f6 * (int64_t) g1; + int64_t f6g2 = f6 * (int64_t) g2; + int64_t f6g3 = f6 * (int64_t) g3; + int64_t f6g4_19 = f6 * (int64_t) g4_19; + int64_t f6g5_19 = f6 * (int64_t) g5_19; + int64_t f6g6_19 = f6 * (int64_t) g6_19; + int64_t f6g7_19 = f6 * (int64_t) g7_19; + int64_t f6g8_19 = f6 * (int64_t) g8_19; + int64_t f6g9_19 = f6 * (int64_t) g9_19; + int64_t f7g0 = f7 * (int64_t) g0; + int64_t f7g1_2 = f7_2 * (int64_t) g1; + int64_t f7g2 = f7 * (int64_t) g2; + int64_t f7g3_38 = f7_2 * (int64_t) g3_19; + int64_t f7g4_19 = f7 * (int64_t) g4_19; + int64_t f7g5_38 = f7_2 * (int64_t) g5_19; + int64_t f7g6_19 = f7 * (int64_t) g6_19; + int64_t f7g7_38 = f7_2 * (int64_t) g7_19; + int64_t f7g8_19 = f7 * (int64_t) g8_19; + int64_t f7g9_38 = f7_2 * (int64_t) g9_19; + int64_t f8g0 = f8 * (int64_t) g0; + int64_t f8g1 = f8 * (int64_t) g1; + int64_t f8g2_19 = f8 * (int64_t) g2_19; + int64_t f8g3_19 = f8 * (int64_t) g3_19; + int64_t f8g4_19 = f8 * (int64_t) g4_19; + int64_t f8g5_19 = f8 * (int64_t) g5_19; + int64_t f8g6_19 = f8 * (int64_t) g6_19; + int64_t f8g7_19 = f8 * (int64_t) g7_19; + int64_t f8g8_19 = f8 * (int64_t) g8_19; + int64_t f8g9_19 = f8 * (int64_t) g9_19; + int64_t f9g0 = f9 * (int64_t) g0; + int64_t f9g1_38 = f9_2 * (int64_t) g1_19; + int64_t f9g2_19 = f9 * (int64_t) g2_19; + int64_t f9g3_38 = f9_2 * (int64_t) g3_19; + int64_t f9g4_19 = f9 * (int64_t) g4_19; + int64_t f9g5_38 = f9_2 * (int64_t) g5_19; + int64_t f9g6_19 = f9 * (int64_t) g6_19; + int64_t f9g7_38 = f9_2 * (int64_t) g7_19; + int64_t f9g8_19 = f9 * (int64_t) g8_19; + int64_t f9g9_38 = f9_2 * (int64_t) g9_19; + int64_t h0 = f0g0 + f1g9_38 + f2g8_19 + f3g7_38 + f4g6_19 + f5g5_38 + f6g4_19 + f7g3_38 + f8g2_19 + f9g1_38; + int64_t h1 = f0g1 + f1g0 + f2g9_19 + f3g8_19 + f4g7_19 + f5g6_19 + f6g5_19 + f7g4_19 + f8g3_19 + f9g2_19; + int64_t h2 = f0g2 + f1g1_2 + f2g0 + f3g9_38 + f4g8_19 + f5g7_38 + f6g6_19 + f7g5_38 + f8g4_19 + f9g3_38; + int64_t h3 = f0g3 + f1g2 + f2g1 + f3g0 + f4g9_19 + f5g8_19 + f6g7_19 + f7g6_19 + f8g5_19 + f9g4_19; + int64_t h4 = f0g4 + f1g3_2 + f2g2 + f3g1_2 + f4g0 + f5g9_38 + f6g8_19 + f7g7_38 + f8g6_19 + f9g5_38; + int64_t h5 = f0g5 + f1g4 + f2g3 + f3g2 + f4g1 + f5g0 + f6g9_19 + f7g8_19 + f8g7_19 + f9g6_19; + int64_t h6 = f0g6 + f1g5_2 + f2g4 + f3g3_2 + f4g2 + f5g1_2 + f6g0 + f7g9_38 + f8g8_19 + f9g7_38; + int64_t h7 = f0g7 + f1g6 + f2g5 + f3g4 + f4g3 + f5g2 + f6g1 + f7g0 + f8g9_19 + f9g8_19; + int64_t h8 = f0g8 + f1g7_2 + f2g6 + f3g5_2 + f4g4 + f5g3_2 + f6g2 + f7g1_2 + f8g0 + f9g9_38; + int64_t h9 = f0g9 + f1g8 + f2g7 + f3g6 + f4g5 + f5g4 + f6g3 + f7g2 + f8g1 + f9g0 ; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + + carry1 = (h1 + (int64_t) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (int64_t) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + + carry2 = (h2 + (int64_t) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (int64_t) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + + carry3 = (h3 + (int64_t) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (int64_t) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (int64_t) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + carry9 = (h9 + (int64_t) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + + +/* +h = f * 121666 +Can overlap h with f. + +Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + +Postconditions: + |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +*/ + +void fe_mul121666(fe h, fe f) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int64_t h0 = f0 * (int64_t) 121666; + int64_t h1 = f1 * (int64_t) 121666; + int64_t h2 = f2 * (int64_t) 121666; + int64_t h3 = f3 * (int64_t) 121666; + int64_t h4 = f4 * (int64_t) 121666; + int64_t h5 = f5 * (int64_t) 121666; + int64_t h6 = f6 * (int64_t) 121666; + int64_t h7 = f7 * (int64_t) 121666; + int64_t h8 = f8 * (int64_t) 121666; + int64_t h9 = f9 * (int64_t) 121666; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + carry9 = (h9 + (int64_t) (1<<24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; + carry1 = (h1 + (int64_t) (1<<24)) >> 25; h2 += carry1; h1 -= carry1 << 25; + carry3 = (h3 + (int64_t) (1<<24)) >> 25; h4 += carry3; h3 -= carry3 << 25; + carry5 = (h5 + (int64_t) (1<<24)) >> 25; h6 += carry5; h5 -= carry5 << 25; + carry7 = (h7 + (int64_t) (1<<24)) >> 25; h8 += carry7; h7 -= carry7 << 25; + + carry0 = (h0 + (int64_t) (1<<25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + carry2 = (h2 + (int64_t) (1<<25)) >> 26; h3 += carry2; h2 -= carry2 << 26; + carry4 = (h4 + (int64_t) (1<<25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + carry6 = (h6 + (int64_t) (1<<25)) >> 26; h7 += carry6; h6 -= carry6 << 26; + carry8 = (h8 + (int64_t) (1<<25)) >> 26; h9 += carry8; h8 -= carry8 << 26; + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + + +/* +h = -f + +Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + +Postconditions: + |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +*/ + +void fe_neg(fe h, const fe f) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t h0 = -f0; + int32_t h1 = -f1; + int32_t h2 = -f2; + int32_t h3 = -f3; + int32_t h4 = -f4; + int32_t h5 = -f5; + int32_t h6 = -f6; + int32_t h7 = -f7; + int32_t h8 = -f8; + int32_t h9 = -f9; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + + +void fe_pow22523(fe out, const fe z) { + fe t0; + fe t1; + fe t2; + int i; + fe_sq(t0, z); + + for (i = 1; i < 1; ++i) { + fe_sq(t0, t0); + } + + fe_sq(t1, t0); + + for (i = 1; i < 2; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t1, z, t1); + fe_mul(t0, t0, t1); + fe_sq(t0, t0); + + for (i = 1; i < 1; ++i) { + fe_sq(t0, t0); + } + + fe_mul(t0, t1, t0); + fe_sq(t1, t0); + + for (i = 1; i < 5; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t0, t1, t0); + fe_sq(t1, t0); + + for (i = 1; i < 10; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t1, t1, t0); + fe_sq(t2, t1); + + for (i = 1; i < 20; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t2, t1); + fe_sq(t1, t1); + + for (i = 1; i < 10; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t0, t1, t0); + fe_sq(t1, t0); + + for (i = 1; i < 50; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t1, t1, t0); + fe_sq(t2, t1); + + for (i = 1; i < 100; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t2, t1); + fe_sq(t1, t1); + + for (i = 1; i < 50; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t0, t1, t0); + fe_sq(t0, t0); + + for (i = 1; i < 2; ++i) { + fe_sq(t0, t0); + } + + fe_mul(out, t0, z); + return; +} + + +/* +h = f * f +Can overlap h with f. + +Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + +Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. +*/ + +/* +See fe_mul.c for discussion of implementation strategy. +*/ + +void fe_sq(fe h, const fe f) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t f0_2 = 2 * f0; + int32_t f1_2 = 2 * f1; + int32_t f2_2 = 2 * f2; + int32_t f3_2 = 2 * f3; + int32_t f4_2 = 2 * f4; + int32_t f5_2 = 2 * f5; + int32_t f6_2 = 2 * f6; + int32_t f7_2 = 2 * f7; + int32_t f5_38 = 38 * f5; /* 1.959375*2^30 */ + int32_t f6_19 = 19 * f6; /* 1.959375*2^30 */ + int32_t f7_38 = 38 * f7; /* 1.959375*2^30 */ + int32_t f8_19 = 19 * f8; /* 1.959375*2^30 */ + int32_t f9_38 = 38 * f9; /* 1.959375*2^30 */ + int64_t f0f0 = f0 * (int64_t) f0; + int64_t f0f1_2 = f0_2 * (int64_t) f1; + int64_t f0f2_2 = f0_2 * (int64_t) f2; + int64_t f0f3_2 = f0_2 * (int64_t) f3; + int64_t f0f4_2 = f0_2 * (int64_t) f4; + int64_t f0f5_2 = f0_2 * (int64_t) f5; + int64_t f0f6_2 = f0_2 * (int64_t) f6; + int64_t f0f7_2 = f0_2 * (int64_t) f7; + int64_t f0f8_2 = f0_2 * (int64_t) f8; + int64_t f0f9_2 = f0_2 * (int64_t) f9; + int64_t f1f1_2 = f1_2 * (int64_t) f1; + int64_t f1f2_2 = f1_2 * (int64_t) f2; + int64_t f1f3_4 = f1_2 * (int64_t) f3_2; + int64_t f1f4_2 = f1_2 * (int64_t) f4; + int64_t f1f5_4 = f1_2 * (int64_t) f5_2; + int64_t f1f6_2 = f1_2 * (int64_t) f6; + int64_t f1f7_4 = f1_2 * (int64_t) f7_2; + int64_t f1f8_2 = f1_2 * (int64_t) f8; + int64_t f1f9_76 = f1_2 * (int64_t) f9_38; + int64_t f2f2 = f2 * (int64_t) f2; + int64_t f2f3_2 = f2_2 * (int64_t) f3; + int64_t f2f4_2 = f2_2 * (int64_t) f4; + int64_t f2f5_2 = f2_2 * (int64_t) f5; + int64_t f2f6_2 = f2_2 * (int64_t) f6; + int64_t f2f7_2 = f2_2 * (int64_t) f7; + int64_t f2f8_38 = f2_2 * (int64_t) f8_19; + int64_t f2f9_38 = f2 * (int64_t) f9_38; + int64_t f3f3_2 = f3_2 * (int64_t) f3; + int64_t f3f4_2 = f3_2 * (int64_t) f4; + int64_t f3f5_4 = f3_2 * (int64_t) f5_2; + int64_t f3f6_2 = f3_2 * (int64_t) f6; + int64_t f3f7_76 = f3_2 * (int64_t) f7_38; + int64_t f3f8_38 = f3_2 * (int64_t) f8_19; + int64_t f3f9_76 = f3_2 * (int64_t) f9_38; + int64_t f4f4 = f4 * (int64_t) f4; + int64_t f4f5_2 = f4_2 * (int64_t) f5; + int64_t f4f6_38 = f4_2 * (int64_t) f6_19; + int64_t f4f7_38 = f4 * (int64_t) f7_38; + int64_t f4f8_38 = f4_2 * (int64_t) f8_19; + int64_t f4f9_38 = f4 * (int64_t) f9_38; + int64_t f5f5_38 = f5 * (int64_t) f5_38; + int64_t f5f6_38 = f5_2 * (int64_t) f6_19; + int64_t f5f7_76 = f5_2 * (int64_t) f7_38; + int64_t f5f8_38 = f5_2 * (int64_t) f8_19; + int64_t f5f9_76 = f5_2 * (int64_t) f9_38; + int64_t f6f6_19 = f6 * (int64_t) f6_19; + int64_t f6f7_38 = f6 * (int64_t) f7_38; + int64_t f6f8_38 = f6_2 * (int64_t) f8_19; + int64_t f6f9_38 = f6 * (int64_t) f9_38; + int64_t f7f7_38 = f7 * (int64_t) f7_38; + int64_t f7f8_38 = f7_2 * (int64_t) f8_19; + int64_t f7f9_76 = f7_2 * (int64_t) f9_38; + int64_t f8f8_19 = f8 * (int64_t) f8_19; + int64_t f8f9_38 = f8 * (int64_t) f9_38; + int64_t f9f9_38 = f9 * (int64_t) f9_38; + int64_t h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + int64_t h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + int64_t h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + int64_t h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + int64_t h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + int64_t h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + int64_t h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + int64_t h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + int64_t h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + int64_t h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry1 = (h1 + (int64_t) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (int64_t) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry2 = (h2 + (int64_t) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (int64_t) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry3 = (h3 + (int64_t) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (int64_t) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (int64_t) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + carry9 = (h9 + (int64_t) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + + +/* +h = 2 * f * f +Can overlap h with f. + +Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + +Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. +*/ + +/* +See fe_mul.c for discussion of implementation strategy. +*/ + +void fe_sq2(fe h, const fe f) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t f0_2 = 2 * f0; + int32_t f1_2 = 2 * f1; + int32_t f2_2 = 2 * f2; + int32_t f3_2 = 2 * f3; + int32_t f4_2 = 2 * f4; + int32_t f5_2 = 2 * f5; + int32_t f6_2 = 2 * f6; + int32_t f7_2 = 2 * f7; + int32_t f5_38 = 38 * f5; /* 1.959375*2^30 */ + int32_t f6_19 = 19 * f6; /* 1.959375*2^30 */ + int32_t f7_38 = 38 * f7; /* 1.959375*2^30 */ + int32_t f8_19 = 19 * f8; /* 1.959375*2^30 */ + int32_t f9_38 = 38 * f9; /* 1.959375*2^30 */ + int64_t f0f0 = f0 * (int64_t) f0; + int64_t f0f1_2 = f0_2 * (int64_t) f1; + int64_t f0f2_2 = f0_2 * (int64_t) f2; + int64_t f0f3_2 = f0_2 * (int64_t) f3; + int64_t f0f4_2 = f0_2 * (int64_t) f4; + int64_t f0f5_2 = f0_2 * (int64_t) f5; + int64_t f0f6_2 = f0_2 * (int64_t) f6; + int64_t f0f7_2 = f0_2 * (int64_t) f7; + int64_t f0f8_2 = f0_2 * (int64_t) f8; + int64_t f0f9_2 = f0_2 * (int64_t) f9; + int64_t f1f1_2 = f1_2 * (int64_t) f1; + int64_t f1f2_2 = f1_2 * (int64_t) f2; + int64_t f1f3_4 = f1_2 * (int64_t) f3_2; + int64_t f1f4_2 = f1_2 * (int64_t) f4; + int64_t f1f5_4 = f1_2 * (int64_t) f5_2; + int64_t f1f6_2 = f1_2 * (int64_t) f6; + int64_t f1f7_4 = f1_2 * (int64_t) f7_2; + int64_t f1f8_2 = f1_2 * (int64_t) f8; + int64_t f1f9_76 = f1_2 * (int64_t) f9_38; + int64_t f2f2 = f2 * (int64_t) f2; + int64_t f2f3_2 = f2_2 * (int64_t) f3; + int64_t f2f4_2 = f2_2 * (int64_t) f4; + int64_t f2f5_2 = f2_2 * (int64_t) f5; + int64_t f2f6_2 = f2_2 * (int64_t) f6; + int64_t f2f7_2 = f2_2 * (int64_t) f7; + int64_t f2f8_38 = f2_2 * (int64_t) f8_19; + int64_t f2f9_38 = f2 * (int64_t) f9_38; + int64_t f3f3_2 = f3_2 * (int64_t) f3; + int64_t f3f4_2 = f3_2 * (int64_t) f4; + int64_t f3f5_4 = f3_2 * (int64_t) f5_2; + int64_t f3f6_2 = f3_2 * (int64_t) f6; + int64_t f3f7_76 = f3_2 * (int64_t) f7_38; + int64_t f3f8_38 = f3_2 * (int64_t) f8_19; + int64_t f3f9_76 = f3_2 * (int64_t) f9_38; + int64_t f4f4 = f4 * (int64_t) f4; + int64_t f4f5_2 = f4_2 * (int64_t) f5; + int64_t f4f6_38 = f4_2 * (int64_t) f6_19; + int64_t f4f7_38 = f4 * (int64_t) f7_38; + int64_t f4f8_38 = f4_2 * (int64_t) f8_19; + int64_t f4f9_38 = f4 * (int64_t) f9_38; + int64_t f5f5_38 = f5 * (int64_t) f5_38; + int64_t f5f6_38 = f5_2 * (int64_t) f6_19; + int64_t f5f7_76 = f5_2 * (int64_t) f7_38; + int64_t f5f8_38 = f5_2 * (int64_t) f8_19; + int64_t f5f9_76 = f5_2 * (int64_t) f9_38; + int64_t f6f6_19 = f6 * (int64_t) f6_19; + int64_t f6f7_38 = f6 * (int64_t) f7_38; + int64_t f6f8_38 = f6_2 * (int64_t) f8_19; + int64_t f6f9_38 = f6 * (int64_t) f9_38; + int64_t f7f7_38 = f7 * (int64_t) f7_38; + int64_t f7f8_38 = f7_2 * (int64_t) f8_19; + int64_t f7f9_76 = f7_2 * (int64_t) f9_38; + int64_t f8f8_19 = f8 * (int64_t) f8_19; + int64_t f8f9_38 = f8 * (int64_t) f9_38; + int64_t f9f9_38 = f9 * (int64_t) f9_38; + int64_t h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + int64_t h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + int64_t h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + int64_t h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + int64_t h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + int64_t h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + int64_t h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + int64_t h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + int64_t h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + int64_t h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + h0 += h0; + h1 += h1; + h2 += h2; + h3 += h3; + h4 += h4; + h5 += h5; + h6 += h6; + h7 += h7; + h8 += h8; + h9 += h9; + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry1 = (h1 + (int64_t) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (int64_t) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry2 = (h2 + (int64_t) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (int64_t) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry3 = (h3 + (int64_t) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (int64_t) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (int64_t) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + carry9 = (h9 + (int64_t) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + + +/* +h = f - g +Can overlap h with f or g. + +Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + +Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +*/ + +void fe_sub(fe h, const fe f, const fe g) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + int32_t h0 = f0 - g0; + int32_t h1 = f1 - g1; + int32_t h2 = f2 - g2; + int32_t h3 = f3 - g3; + int32_t h4 = f4 - g4; + int32_t h5 = f5 - g5; + int32_t h6 = f6 - g6; + int32_t h7 = f7 - g7; + int32_t h8 = f8 - g8; + int32_t h9 = f9 - g9; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + + + +/* +Preconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + +Write p=2^255-19; q=floor(h/p). +Basic claim: q = floor(2^(-255)(h + 19 2^(-25)h9 + 2^(-1))). + +Proof: + Have |h|<=p so |q|<=1 so |19^2 2^(-255) q|<1/4. + Also have |h-2^230 h9|<2^231 so |19 2^(-255)(h-2^230 h9)|<1/4. + + Write y=2^(-1)-19^2 2^(-255)q-19 2^(-255)(h-2^230 h9). + Then 0> 25; + q = (h0 + q) >> 26; + q = (h1 + q) >> 25; + q = (h2 + q) >> 26; + q = (h3 + q) >> 25; + q = (h4 + q) >> 26; + q = (h5 + q) >> 25; + q = (h6 + q) >> 26; + q = (h7 + q) >> 25; + q = (h8 + q) >> 26; + q = (h9 + q) >> 25; + /* Goal: Output h-(2^255-19)q, which is between 0 and 2^255-20. */ + h0 += 19 * q; + /* Goal: Output h-2^255 q, which is between 0 and 2^255-20. */ + carry0 = h0 >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry1 = h1 >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry2 = h2 >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry3 = h3 >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry4 = h4 >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry5 = h5 >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry6 = h6 >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry7 = h7 >> 25; + h8 += carry7; + h7 -= carry7 << 25; + carry8 = h8 >> 26; + h9 += carry8; + h8 -= carry8 << 26; + carry9 = h9 >> 25; + h9 -= carry9 << 25; + + /* h10 = carry9 */ + /* + Goal: Output h0+...+2^255 h10-2^255 q, which is between 0 and 2^255-20. + Have h0+...+2^230 h9 between 0 and 2^255-1; + evidently 2^255 h10-2^255 q = 0. + Goal: Output h0+...+2^230 h9. + */ + s[0] = (unsigned char) (h0 >> 0); + s[1] = (unsigned char) (h0 >> 8); + s[2] = (unsigned char) (h0 >> 16); + s[3] = (unsigned char) ((h0 >> 24) | (h1 << 2)); + s[4] = (unsigned char) (h1 >> 6); + s[5] = (unsigned char) (h1 >> 14); + s[6] = (unsigned char) ((h1 >> 22) | (h2 << 3)); + s[7] = (unsigned char) (h2 >> 5); + s[8] = (unsigned char) (h2 >> 13); + s[9] = (unsigned char) ((h2 >> 21) | (h3 << 5)); + s[10] = (unsigned char) (h3 >> 3); + s[11] = (unsigned char) (h3 >> 11); + s[12] = (unsigned char) ((h3 >> 19) | (h4 << 6)); + s[13] = (unsigned char) (h4 >> 2); + s[14] = (unsigned char) (h4 >> 10); + s[15] = (unsigned char) (h4 >> 18); + s[16] = (unsigned char) (h5 >> 0); + s[17] = (unsigned char) (h5 >> 8); + s[18] = (unsigned char) (h5 >> 16); + s[19] = (unsigned char) ((h5 >> 24) | (h6 << 1)); + s[20] = (unsigned char) (h6 >> 7); + s[21] = (unsigned char) (h6 >> 15); + s[22] = (unsigned char) ((h6 >> 23) | (h7 << 3)); + s[23] = (unsigned char) (h7 >> 5); + s[24] = (unsigned char) (h7 >> 13); + s[25] = (unsigned char) ((h7 >> 21) | (h8 << 4)); + s[26] = (unsigned char) (h8 >> 4); + s[27] = (unsigned char) (h8 >> 12); + s[28] = (unsigned char) ((h8 >> 20) | (h9 << 6)); + s[29] = (unsigned char) (h9 >> 2); + s[30] = (unsigned char) (h9 >> 10); + s[31] = (unsigned char) (h9 >> 18); +} diff --git a/zephcore/lib/ed25519/fe.h b/zephcore/lib/ed25519/fe.h new file mode 100644 index 0000000..b4b62d2 --- /dev/null +++ b/zephcore/lib/ed25519/fe.h @@ -0,0 +1,41 @@ +#ifndef FE_H +#define FE_H + +#include "fixedint.h" + + +/* + fe means field element. + Here the field is \Z/(2^255-19). + An element t, entries t[0]...t[9], represents the integer + t[0]+2^26 t[1]+2^51 t[2]+2^77 t[3]+2^102 t[4]+...+2^230 t[9]. + Bounds on each t[i] vary depending on context. +*/ + + +typedef int32_t fe[10]; + + +void fe_0(fe h); +void fe_1(fe h); + +void fe_frombytes(fe h, const unsigned char *s); +void fe_tobytes(unsigned char *s, const fe h); + +void fe_copy(fe h, const fe f); +int fe_isnegative(const fe f); +int fe_isnonzero(const fe f); +void fe_cmov(fe f, const fe g, unsigned int b); +void fe_cswap(fe f, fe g, unsigned int b); + +void fe_neg(fe h, const fe f); +void fe_add(fe h, const fe f, const fe g); +void fe_invert(fe out, const fe z); +void fe_sq(fe h, const fe f); +void fe_sq2(fe h, const fe f); +void fe_mul(fe h, const fe f, const fe g); +void fe_mul121666(fe h, fe f); +void fe_pow22523(fe out, const fe z); +void fe_sub(fe h, const fe f, const fe g); + +#endif diff --git a/zephcore/lib/ed25519/fixedint.h b/zephcore/lib/ed25519/fixedint.h new file mode 100644 index 0000000..1a8745b --- /dev/null +++ b/zephcore/lib/ed25519/fixedint.h @@ -0,0 +1,72 @@ +/* + Portable header to provide the 32 and 64 bits type. + + Not a compatible replacement for , do not blindly use it as such. +*/ + +#if ((defined(__STDC__) && __STDC__ && __STDC_VERSION__ >= 199901L) || (defined(__WATCOMC__) && (defined(_STDINT_H_INCLUDED) || __WATCOMC__ >= 1250)) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_) || defined(__UINT_FAST64_TYPE__)) )) && !defined(FIXEDINT_H_INCLUDED) + #include + #define FIXEDINT_H_INCLUDED + + #if defined(__WATCOMC__) && __WATCOMC__ >= 1250 && !defined(UINT64_C) + #include + #define UINT64_C(x) (x + (UINT64_MAX - UINT64_MAX)) + #endif +#endif + + +#ifndef FIXEDINT_H_INCLUDED + #define FIXEDINT_H_INCLUDED + + #include + + /* (u)int32_t */ + #ifndef uint32_t + #if (ULONG_MAX == 0xffffffffUL) + typedef unsigned long uint32_t; + #elif (UINT_MAX == 0xffffffffUL) + typedef unsigned int uint32_t; + #elif (USHRT_MAX == 0xffffffffUL) + typedef unsigned short uint32_t; + #endif + #endif + + + #ifndef int32_t + #if (LONG_MAX == 0x7fffffffL) + typedef signed long int32_t; + #elif (INT_MAX == 0x7fffffffL) + typedef signed int int32_t; + #elif (SHRT_MAX == 0x7fffffffL) + typedef signed short int32_t; + #endif + #endif + + + /* (u)int64_t */ + #if (defined(__STDC__) && defined(__STDC_VERSION__) && __STDC__ && __STDC_VERSION__ >= 199901L) + typedef long long int64_t; + typedef unsigned long long uint64_t; + + #define UINT64_C(v) v ##ULL + #define INT64_C(v) v ##LL + #elif defined(__GNUC__) + __extension__ typedef long long int64_t; + __extension__ typedef unsigned long long uint64_t; + + #define UINT64_C(v) v ##ULL + #define INT64_C(v) v ##LL + #elif defined(__MWERKS__) || defined(__SUNPRO_C) || defined(__SUNPRO_CC) || defined(__APPLE_CC__) || defined(_LONG_LONG) || defined(_CRAYC) + typedef long long int64_t; + typedef unsigned long long uint64_t; + + #define UINT64_C(v) v ##ULL + #define INT64_C(v) v ##LL + #elif (defined(__WATCOMC__) && defined(__WATCOM_INT64__)) || (defined(_MSC_VER) && _INTEGRAL_MAX_BITS >= 64) || (defined(__BORLANDC__) && __BORLANDC__ > 0x460) || defined(__alpha) || defined(__DECC) + typedef __int64 int64_t; + typedef unsigned __int64 uint64_t; + + #define UINT64_C(v) v ##UI64 + #define INT64_C(v) v ##I64 + #endif +#endif diff --git a/zephcore/lib/ed25519/ge.c b/zephcore/lib/ed25519/ge.c new file mode 100644 index 0000000..87c691b --- /dev/null +++ b/zephcore/lib/ed25519/ge.c @@ -0,0 +1,467 @@ +#include "ge.h" +#include "precomp_data.h" + + +/* +r = p + q +*/ + +void ge_add(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q) { + fe t0; + fe_add(r->X, p->Y, p->X); + fe_sub(r->Y, p->Y, p->X); + fe_mul(r->Z, r->X, q->YplusX); + fe_mul(r->Y, r->Y, q->YminusX); + fe_mul(r->T, q->T2d, p->T); + fe_mul(r->X, p->Z, q->Z); + fe_add(t0, r->X, r->X); + fe_sub(r->X, r->Z, r->Y); + fe_add(r->Y, r->Z, r->Y); + fe_add(r->Z, t0, r->T); + fe_sub(r->T, t0, r->T); +} + + +static void slide(signed char *r, const unsigned char *a) { + int i; + int b; + int k; + + for (i = 0; i < 256; ++i) { + r[i] = 1 & (a[i >> 3] >> (i & 7)); + } + + for (i = 0; i < 256; ++i) + if (r[i]) { + for (b = 1; b <= 6 && i + b < 256; ++b) { + if (r[i + b]) { + if (r[i] + (r[i + b] << b) <= 15) { + r[i] += r[i + b] << b; + r[i + b] = 0; + } else if (r[i] - (r[i + b] << b) >= -15) { + r[i] -= r[i + b] << b; + + for (k = i + b; k < 256; ++k) { + if (!r[k]) { + r[k] = 1; + break; + } + + r[k] = 0; + } + } else { + break; + } + } + } + } +} + +/* +r = a * A + b * B +where a = a[0]+256*a[1]+...+256^31 a[31]. +and b = b[0]+256*b[1]+...+256^31 b[31]. +B is the Ed25519 base point (x,4/5) with x positive. +*/ + +void ge_double_scalarmult_vartime(ge_p2 *r, const unsigned char *a, const ge_p3 *A, const unsigned char *b) { + signed char aslide[256]; + signed char bslide[256]; + ge_cached Ai[8]; /* A,3A,5A,7A,9A,11A,13A,15A */ + ge_p1p1 t; + ge_p3 u; + ge_p3 A2; + int i; + slide(aslide, a); + slide(bslide, b); + ge_p3_to_cached(&Ai[0], A); + ge_p3_dbl(&t, A); + ge_p1p1_to_p3(&A2, &t); + ge_add(&t, &A2, &Ai[0]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[1], &u); + ge_add(&t, &A2, &Ai[1]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[2], &u); + ge_add(&t, &A2, &Ai[2]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[3], &u); + ge_add(&t, &A2, &Ai[3]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[4], &u); + ge_add(&t, &A2, &Ai[4]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[5], &u); + ge_add(&t, &A2, &Ai[5]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[6], &u); + ge_add(&t, &A2, &Ai[6]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[7], &u); + ge_p2_0(r); + + for (i = 255; i >= 0; --i) { + if (aslide[i] || bslide[i]) { + break; + } + } + + for (; i >= 0; --i) { + ge_p2_dbl(&t, r); + + if (aslide[i] > 0) { + ge_p1p1_to_p3(&u, &t); + ge_add(&t, &u, &Ai[aslide[i] / 2]); + } else if (aslide[i] < 0) { + ge_p1p1_to_p3(&u, &t); + ge_sub(&t, &u, &Ai[(-aslide[i]) / 2]); + } + + if (bslide[i] > 0) { + ge_p1p1_to_p3(&u, &t); + ge_madd(&t, &u, &Bi[bslide[i] / 2]); + } else if (bslide[i] < 0) { + ge_p1p1_to_p3(&u, &t); + ge_msub(&t, &u, &Bi[(-bslide[i]) / 2]); + } + + ge_p1p1_to_p2(r, &t); + } +} + + +static const fe d = { + -10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116 +}; + +static const fe sqrtm1 = { + -32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482 +}; + +int ge_frombytes_negate_vartime(ge_p3 *h, const unsigned char *s) { + fe u; + fe v; + fe v3; + fe vxx; + fe check; + fe_frombytes(h->Y, s); + fe_1(h->Z); + fe_sq(u, h->Y); + fe_mul(v, u, d); + fe_sub(u, u, h->Z); /* u = y^2-1 */ + fe_add(v, v, h->Z); /* v = dy^2+1 */ + fe_sq(v3, v); + fe_mul(v3, v3, v); /* v3 = v^3 */ + fe_sq(h->X, v3); + fe_mul(h->X, h->X, v); + fe_mul(h->X, h->X, u); /* x = uv^7 */ + fe_pow22523(h->X, h->X); /* x = (uv^7)^((q-5)/8) */ + fe_mul(h->X, h->X, v3); + fe_mul(h->X, h->X, u); /* x = uv^3(uv^7)^((q-5)/8) */ + fe_sq(vxx, h->X); + fe_mul(vxx, vxx, v); + fe_sub(check, vxx, u); /* vx^2-u */ + + if (fe_isnonzero(check)) { + fe_add(check, vxx, u); /* vx^2+u */ + + if (fe_isnonzero(check)) { + return -1; + } + + fe_mul(h->X, h->X, sqrtm1); + } + + if (fe_isnegative(h->X) == (s[31] >> 7)) { + fe_neg(h->X, h->X); + } + + fe_mul(h->T, h->X, h->Y); + return 0; +} + + +/* +r = p + q +*/ + +void ge_madd(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q) { + fe t0; + fe_add(r->X, p->Y, p->X); + fe_sub(r->Y, p->Y, p->X); + fe_mul(r->Z, r->X, q->yplusx); + fe_mul(r->Y, r->Y, q->yminusx); + fe_mul(r->T, q->xy2d, p->T); + fe_add(t0, p->Z, p->Z); + fe_sub(r->X, r->Z, r->Y); + fe_add(r->Y, r->Z, r->Y); + fe_add(r->Z, t0, r->T); + fe_sub(r->T, t0, r->T); +} + + +/* +r = p - q +*/ + +void ge_msub(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q) { + fe t0; + + fe_add(r->X, p->Y, p->X); + fe_sub(r->Y, p->Y, p->X); + fe_mul(r->Z, r->X, q->yminusx); + fe_mul(r->Y, r->Y, q->yplusx); + fe_mul(r->T, q->xy2d, p->T); + fe_add(t0, p->Z, p->Z); + fe_sub(r->X, r->Z, r->Y); + fe_add(r->Y, r->Z, r->Y); + fe_sub(r->Z, t0, r->T); + fe_add(r->T, t0, r->T); +} + + +/* +r = p +*/ + +void ge_p1p1_to_p2(ge_p2 *r, const ge_p1p1 *p) { + fe_mul(r->X, p->X, p->T); + fe_mul(r->Y, p->Y, p->Z); + fe_mul(r->Z, p->Z, p->T); +} + + + +/* +r = p +*/ + +void ge_p1p1_to_p3(ge_p3 *r, const ge_p1p1 *p) { + fe_mul(r->X, p->X, p->T); + fe_mul(r->Y, p->Y, p->Z); + fe_mul(r->Z, p->Z, p->T); + fe_mul(r->T, p->X, p->Y); +} + + +void ge_p2_0(ge_p2 *h) { + fe_0(h->X); + fe_1(h->Y); + fe_1(h->Z); +} + + + +/* +r = 2 * p +*/ + +void ge_p2_dbl(ge_p1p1 *r, const ge_p2 *p) { + fe t0; + + fe_sq(r->X, p->X); + fe_sq(r->Z, p->Y); + fe_sq2(r->T, p->Z); + fe_add(r->Y, p->X, p->Y); + fe_sq(t0, r->Y); + fe_add(r->Y, r->Z, r->X); + fe_sub(r->Z, r->Z, r->X); + fe_sub(r->X, t0, r->Y); + fe_sub(r->T, r->T, r->Z); +} + + +void ge_p3_0(ge_p3 *h) { + fe_0(h->X); + fe_1(h->Y); + fe_1(h->Z); + fe_0(h->T); +} + + +/* +r = 2 * p +*/ + +void ge_p3_dbl(ge_p1p1 *r, const ge_p3 *p) { + ge_p2 q; + ge_p3_to_p2(&q, p); + ge_p2_dbl(r, &q); +} + + + +/* +r = p +*/ + +static const fe d2 = { + -21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199 +}; + +void ge_p3_to_cached(ge_cached *r, const ge_p3 *p) { + fe_add(r->YplusX, p->Y, p->X); + fe_sub(r->YminusX, p->Y, p->X); + fe_copy(r->Z, p->Z); + fe_mul(r->T2d, p->T, d2); +} + + +/* +r = p +*/ + +void ge_p3_to_p2(ge_p2 *r, const ge_p3 *p) { + fe_copy(r->X, p->X); + fe_copy(r->Y, p->Y); + fe_copy(r->Z, p->Z); +} + + +void ge_p3_tobytes(unsigned char *s, const ge_p3 *h) { + fe recip; + fe x; + fe y; + fe_invert(recip, h->Z); + fe_mul(x, h->X, recip); + fe_mul(y, h->Y, recip); + fe_tobytes(s, y); + s[31] ^= fe_isnegative(x) << 7; +} + + +static unsigned char equal(signed char b, signed char c) { + unsigned char ub = b; + unsigned char uc = c; + unsigned char x = ub ^ uc; /* 0: yes; 1..255: no */ + uint64_t y = x; /* 0: yes; 1..255: no */ + y -= 1; /* large: yes; 0..254: no */ + y >>= 63; /* 1: yes; 0: no */ + return (unsigned char) y; +} + +static unsigned char negative(signed char b) { + uint64_t x = b; /* 18446744073709551361..18446744073709551615: yes; 0..255: no */ + x >>= 63; /* 1: yes; 0: no */ + return (unsigned char) x; +} + +static void cmov(ge_precomp *t, const ge_precomp *u, unsigned char b) { + fe_cmov(t->yplusx, u->yplusx, b); + fe_cmov(t->yminusx, u->yminusx, b); + fe_cmov(t->xy2d, u->xy2d, b); +} + + +static void select(ge_precomp *t, int pos, signed char b) { + ge_precomp minust; + unsigned char bnegative = negative(b); + unsigned char babs = b - (((-bnegative) & b) << 1); + fe_1(t->yplusx); + fe_1(t->yminusx); + fe_0(t->xy2d); + cmov(t, &base[pos][0], equal(babs, 1)); + cmov(t, &base[pos][1], equal(babs, 2)); + cmov(t, &base[pos][2], equal(babs, 3)); + cmov(t, &base[pos][3], equal(babs, 4)); + cmov(t, &base[pos][4], equal(babs, 5)); + cmov(t, &base[pos][5], equal(babs, 6)); + cmov(t, &base[pos][6], equal(babs, 7)); + cmov(t, &base[pos][7], equal(babs, 8)); + fe_copy(minust.yplusx, t->yminusx); + fe_copy(minust.yminusx, t->yplusx); + fe_neg(minust.xy2d, t->xy2d); + cmov(t, &minust, bnegative); +} + +/* +h = a * B +where a = a[0]+256*a[1]+...+256^31 a[31] +B is the Ed25519 base point (x,4/5) with x positive. + +Preconditions: + a[31] <= 127 +*/ + +void ge_scalarmult_base(ge_p3 *h, const unsigned char *a) { + signed char e[64]; + signed char carry; + ge_p1p1 r; + ge_p2 s; + ge_precomp t; + int i; + + for (i = 0; i < 32; ++i) { + e[2 * i + 0] = (a[i] >> 0) & 15; + e[2 * i + 1] = (a[i] >> 4) & 15; + } + + /* each e[i] is between 0 and 15 */ + /* e[63] is between 0 and 7 */ + carry = 0; + + for (i = 0; i < 63; ++i) { + e[i] += carry; + carry = e[i] + 8; + carry >>= 4; + e[i] -= carry << 4; + } + + e[63] += carry; + /* each e[i] is between -8 and 8 */ + ge_p3_0(h); + + for (i = 1; i < 64; i += 2) { + select(&t, i / 2, e[i]); + ge_madd(&r, h, &t); + ge_p1p1_to_p3(h, &r); + } + + ge_p3_dbl(&r, h); + ge_p1p1_to_p2(&s, &r); + ge_p2_dbl(&r, &s); + ge_p1p1_to_p2(&s, &r); + ge_p2_dbl(&r, &s); + ge_p1p1_to_p2(&s, &r); + ge_p2_dbl(&r, &s); + ge_p1p1_to_p3(h, &r); + + for (i = 0; i < 64; i += 2) { + select(&t, i / 2, e[i]); + ge_madd(&r, h, &t); + ge_p1p1_to_p3(h, &r); + } +} + + +/* +r = p - q +*/ + +void ge_sub(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q) { + fe t0; + + fe_add(r->X, p->Y, p->X); + fe_sub(r->Y, p->Y, p->X); + fe_mul(r->Z, r->X, q->YminusX); + fe_mul(r->Y, r->Y, q->YplusX); + fe_mul(r->T, q->T2d, p->T); + fe_mul(r->X, p->Z, q->Z); + fe_add(t0, r->X, r->X); + fe_sub(r->X, r->Z, r->Y); + fe_add(r->Y, r->Z, r->Y); + fe_sub(r->Z, t0, r->T); + fe_add(r->T, t0, r->T); +} + + +void ge_tobytes(unsigned char *s, const ge_p2 *h) { + fe recip; + fe x; + fe y; + fe_invert(recip, h->Z); + fe_mul(x, h->X, recip); + fe_mul(y, h->Y, recip); + fe_tobytes(s, y); + s[31] ^= fe_isnegative(x) << 7; +} diff --git a/zephcore/lib/ed25519/ge.h b/zephcore/lib/ed25519/ge.h new file mode 100644 index 0000000..17fde2d --- /dev/null +++ b/zephcore/lib/ed25519/ge.h @@ -0,0 +1,74 @@ +#ifndef GE_H +#define GE_H + +#include "fe.h" + + +/* +ge means group element. + +Here the group is the set of pairs (x,y) of field elements (see fe.h) +satisfying -x^2 + y^2 = 1 + d x^2y^2 +where d = -121665/121666. + +Representations: + ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z + ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT + ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T + ge_precomp (Duif): (y+x,y-x,2dxy) +*/ + +typedef struct { + fe X; + fe Y; + fe Z; +} ge_p2; + +typedef struct { + fe X; + fe Y; + fe Z; + fe T; +} ge_p3; + +typedef struct { + fe X; + fe Y; + fe Z; + fe T; +} ge_p1p1; + +typedef struct { + fe yplusx; + fe yminusx; + fe xy2d; +} ge_precomp; + +typedef struct { + fe YplusX; + fe YminusX; + fe Z; + fe T2d; +} ge_cached; + +void ge_p3_tobytes(unsigned char *s, const ge_p3 *h); +void ge_tobytes(unsigned char *s, const ge_p2 *h); +int ge_frombytes_negate_vartime(ge_p3 *h, const unsigned char *s); + +void ge_add(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q); +void ge_sub(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q); +void ge_double_scalarmult_vartime(ge_p2 *r, const unsigned char *a, const ge_p3 *A, const unsigned char *b); +void ge_madd(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q); +void ge_msub(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q); +void ge_scalarmult_base(ge_p3 *h, const unsigned char *a); + +void ge_p1p1_to_p2(ge_p2 *r, const ge_p1p1 *p); +void ge_p1p1_to_p3(ge_p3 *r, const ge_p1p1 *p); +void ge_p2_0(ge_p2 *h); +void ge_p2_dbl(ge_p1p1 *r, const ge_p2 *p); +void ge_p3_0(ge_p3 *h); +void ge_p3_dbl(ge_p1p1 *r, const ge_p3 *p); +void ge_p3_to_cached(ge_cached *r, const ge_p3 *p); +void ge_p3_to_p2(ge_p2 *r, const ge_p3 *p); + +#endif diff --git a/zephcore/lib/ed25519/key_exchange.c b/zephcore/lib/ed25519/key_exchange.c new file mode 100644 index 0000000..984c2c8 --- /dev/null +++ b/zephcore/lib/ed25519/key_exchange.c @@ -0,0 +1,79 @@ +#include "ed_25519.h" +#include "fe.h" + +void ed25519_key_exchange(unsigned char *shared_secret, const unsigned char *public_key, const unsigned char *private_key) { + unsigned char e[32]; + unsigned int i; + + fe x1; + fe x2; + fe z2; + fe x3; + fe z3; + fe tmp0; + fe tmp1; + + int pos; + unsigned int swap; + unsigned int b; + + /* copy the private key and make sure it's valid */ + for (i = 0; i < 32; ++i) { + e[i] = private_key[i]; + } + + e[0] &= 248; + e[31] &= 63; + e[31] |= 64; + + /* unpack the public key and convert edwards to montgomery */ + /* due to CodesInChaos: montgomeryX = (edwardsY + 1)*inverse(1 - edwardsY) mod p */ + fe_frombytes(x1, public_key); + fe_1(tmp1); + fe_add(tmp0, x1, tmp1); + fe_sub(tmp1, tmp1, x1); + fe_invert(tmp1, tmp1); + fe_mul(x1, tmp0, tmp1); + + fe_1(x2); + fe_0(z2); + fe_copy(x3, x1); + fe_1(z3); + + swap = 0; + for (pos = 254; pos >= 0; --pos) { + b = e[pos / 8] >> (pos & 7); + b &= 1; + swap ^= b; + fe_cswap(x2, x3, swap); + fe_cswap(z2, z3, swap); + swap = b; + + /* from montgomery.h */ + fe_sub(tmp0, x3, z3); + fe_sub(tmp1, x2, z2); + fe_add(x2, x2, z2); + fe_add(z2, x3, z3); + fe_mul(z3, tmp0, x2); + fe_mul(z2, z2, tmp1); + fe_sq(tmp0, tmp1); + fe_sq(tmp1, x2); + fe_add(x3, z3, z2); + fe_sub(z2, z3, z2); + fe_mul(x2, tmp1, tmp0); + fe_sub(tmp1, tmp1, tmp0); + fe_sq(z2, z2); + fe_mul121666(z3, tmp1); + fe_sq(x3, x3); + fe_add(tmp0, tmp0, z3); + fe_mul(z3, x1, z2); + fe_mul(z2, tmp1, tmp0); + } + + fe_cswap(x2, x3, swap); + fe_cswap(z2, z3, swap); + + fe_invert(z2, z2); + fe_mul(x2, x2, z2); + fe_tobytes(shared_secret, x2); +} diff --git a/zephcore/lib/ed25519/keypair.c b/zephcore/lib/ed25519/keypair.c new file mode 100644 index 0000000..da789f7 --- /dev/null +++ b/zephcore/lib/ed25519/keypair.c @@ -0,0 +1,23 @@ +#include "ed_25519.h" +#include "sha512.h" +#include "ge.h" + + +void ed25519_create_keypair(unsigned char *public_key, unsigned char *private_key, const unsigned char *seed) { + ge_p3 A; + + sha512(seed, 32, private_key); + private_key[0] &= 248; + private_key[31] &= 63; + private_key[31] |= 64; + + ge_scalarmult_base(&A, private_key); + ge_p3_tobytes(public_key, &A); +} + +void ed25519_derive_pub(unsigned char *public_key, const unsigned char *private_key) { + ge_p3 A; + + ge_scalarmult_base(&A, private_key); + ge_p3_tobytes(public_key, &A); +} diff --git a/zephcore/lib/ed25519/license.txt b/zephcore/lib/ed25519/license.txt new file mode 100644 index 0000000..c1503f9 --- /dev/null +++ b/zephcore/lib/ed25519/license.txt @@ -0,0 +1,16 @@ +Copyright (c) 2015 Orson Peters + +This software is provided 'as-is', without any express or implied warranty. In no event will the +authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial +applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the + original software. If you use this software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as + being the original software. + +3. This notice may not be removed or altered from any source distribution. diff --git a/zephcore/lib/ed25519/precomp_data.h b/zephcore/lib/ed25519/precomp_data.h new file mode 100644 index 0000000..ff23986 --- /dev/null +++ b/zephcore/lib/ed25519/precomp_data.h @@ -0,0 +1,1391 @@ +static const ge_precomp Bi[8] = { + { + { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, + { -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378 }, + { -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546 }, + }, + { + { 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024 }, + { 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574 }, + { 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357 }, + }, + { + { 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380 }, + { 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306 }, + { 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942 }, + }, + { + { 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766 }, + { -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701 }, + { 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300 }, + }, + { + { -22518993, -6692182, 14201702, -8745502, -23510406, 8844726, 18474211, -1361450, -13062696, 13821877 }, + { -6455177, -7839871, 3374702, -4740862, -27098617, -10571707, 31655028, -7212327, 18853322, -14220951 }, + { 4566830, -12963868, -28974889, -12240689, -7602672, -2830569, -8514358, -10431137, 2207753, -3209784 }, + }, + { + { -25154831, -4185821, 29681144, 7868801, -6854661, -9423865, -12437364, -663000, -31111463, -16132436 }, + { 25576264, -2703214, 7349804, -11814844, 16472782, 9300885, 3844789, 15725684, 171356, 6466918 }, + { 23103977, 13316479, 9739013, -16149481, 817875, -15038942, 8965339, -14088058, -30714912, 16193877 }, + }, + { + { -33521811, 3180713, -2394130, 14003687, -16903474, -16270840, 17238398, 4729455, -18074513, 9256800 }, + { -25182317, -4174131, 32336398, 5036987, -21236817, 11360617, 22616405, 9761698, -19827198, 630305 }, + { -13720693, 2639453, -24237460, -7406481, 9494427, -5774029, -6554551, -15960994, -2449256, -14291300 }, + }, + { + { -3151181, -5046075, 9282714, 6866145, -31907062, -863023, -18940575, 15033784, 25105118, -7894876 }, + { -24326370, 15950226, -31801215, -14592823, -11662737, -5090925, 1573892, -2625887, 2198790, -15804619 }, + { -3099351, 10324967, -2241613, 7453183, -5446979, -2735503, -13812022, -16236442, -32461234, -12290683 }, + }, +}; + + +/* base[i][j] = (j+1)*256^i*B */ +static const ge_precomp base[32][8] = { + { + { + { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, + { -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378 }, + { -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546 }, + }, + { + { -12815894, -12976347, -21581243, 11784320, -25355658, -2750717, -11717903, -3814571, -358445, -10211303 }, + { -21703237, 6903825, 27185491, 6451973, -29577724, -9554005, -15616551, 11189268, -26829678, -5319081 }, + { 26966642, 11152617, 32442495, 15396054, 14353839, -12752335, -3128826, -9541118, -15472047, -4166697 }, + }, + { + { 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024 }, + { 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574 }, + { 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357 }, + }, + { + { -17036878, 13921892, 10945806, -6033431, 27105052, -16084379, -28926210, 15006023, 3284568, -6276540 }, + { 23599295, -8306047, -11193664, -7687416, 13236774, 10506355, 7464579, 9656445, 13059162, 10374397 }, + { 7798556, 16710257, 3033922, 2874086, 28997861, 2835604, 32406664, -3839045, -641708, -101325 }, + }, + { + { 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380 }, + { 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306 }, + { 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942 }, + }, + { + { -15371964, -12862754, 32573250, 4720197, -26436522, 5875511, -19188627, -15224819, -9818940, -12085777 }, + { -8549212, 109983, 15149363, 2178705, 22900618, 4543417, 3044240, -15689887, 1762328, 14866737 }, + { -18199695, -15951423, -10473290, 1707278, -17185920, 3916101, -28236412, 3959421, 27914454, 4383652 }, + }, + { + { 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766 }, + { -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701 }, + { 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300 }, + }, + { + { 14499471, -2729599, -33191113, -4254652, 28494862, 14271267, 30290735, 10876454, -33154098, 2381726 }, + { -7195431, -2655363, -14730155, 462251, -27724326, 3941372, -6236617, 3696005, -32300832, 15351955 }, + { 27431194, 8222322, 16448760, -3907995, -18707002, 11938355, -32961401, -2970515, 29551813, 10109425 }, + }, + }, + { + { + { -13657040, -13155431, -31283750, 11777098, 21447386, 6519384, -2378284, -1627556, 10092783, -4764171 }, + { 27939166, 14210322, 4677035, 16277044, -22964462, -12398139, -32508754, 12005538, -17810127, 12803510 }, + { 17228999, -15661624, -1233527, 300140, -1224870, -11714777, 30364213, -9038194, 18016357, 4397660 }, + }, + { + { -10958843, -7690207, 4776341, -14954238, 27850028, -15602212, -26619106, 14544525, -17477504, 982639 }, + { 29253598, 15796703, -2863982, -9908884, 10057023, 3163536, 7332899, -4120128, -21047696, 9934963 }, + { 5793303, 16271923, -24131614, -10116404, 29188560, 1206517, -14747930, 4559895, -30123922, -10897950 }, + }, + { + { -27643952, -11493006, 16282657, -11036493, 28414021, -15012264, 24191034, 4541697, -13338309, 5500568 }, + { 12650548, -1497113, 9052871, 11355358, -17680037, -8400164, -17430592, 12264343, 10874051, 13524335 }, + { 25556948, -3045990, 714651, 2510400, 23394682, -10415330, 33119038, 5080568, -22528059, 5376628 }, + }, + { + { -26088264, -4011052, -17013699, -3537628, -6726793, 1920897, -22321305, -9447443, 4535768, 1569007 }, + { -2255422, 14606630, -21692440, -8039818, 28430649, 8775819, -30494562, 3044290, 31848280, 12543772 }, + { -22028579, 2943893, -31857513, 6777306, 13784462, -4292203, -27377195, -2062731, 7718482, 14474653 }, + }, + { + { 2385315, 2454213, -22631320, 46603, -4437935, -15680415, 656965, -7236665, 24316168, -5253567 }, + { 13741529, 10911568, -33233417, -8603737, -20177830, -1033297, 33040651, -13424532, -20729456, 8321686 }, + { 21060490, -2212744, 15712757, -4336099, 1639040, 10656336, 23845965, -11874838, -9984458, 608372 }, + }, + { + { -13672732, -15087586, -10889693, -7557059, -6036909, 11305547, 1123968, -6780577, 27229399, 23887 }, + { -23244140, -294205, -11744728, 14712571, -29465699, -2029617, 12797024, -6440308, -1633405, 16678954 }, + { -29500620, 4770662, -16054387, 14001338, 7830047, 9564805, -1508144, -4795045, -17169265, 4904953 }, + }, + { + { 24059557, 14617003, 19037157, -15039908, 19766093, -14906429, 5169211, 16191880, 2128236, -4326833 }, + { -16981152, 4124966, -8540610, -10653797, 30336522, -14105247, -29806336, 916033, -6882542, -2986532 }, + { -22630907, 12419372, -7134229, -7473371, -16478904, 16739175, 285431, 2763829, 15736322, 4143876 }, + }, + { + { 2379352, 11839345, -4110402, -5988665, 11274298, 794957, 212801, -14594663, 23527084, -16458268 }, + { 33431127, -11130478, -17838966, -15626900, 8909499, 8376530, -32625340, 4087881, -15188911, -14416214 }, + { 1767683, 7197987, -13205226, -2022635, -13091350, 448826, 5799055, 4357868, -4774191, -16323038 }, + }, + }, + { + { + { 6721966, 13833823, -23523388, -1551314, 26354293, -11863321, 23365147, -3949732, 7390890, 2759800 }, + { 4409041, 2052381, 23373853, 10530217, 7676779, -12885954, 21302353, -4264057, 1244380, -12919645 }, + { -4421239, 7169619, 4982368, -2957590, 30256825, -2777540, 14086413, 9208236, 15886429, 16489664 }, + }, + { + { 1996075, 10375649, 14346367, 13311202, -6874135, -16438411, -13693198, 398369, -30606455, -712933 }, + { -25307465, 9795880, -2777414, 14878809, -33531835, 14780363, 13348553, 12076947, -30836462, 5113182 }, + { -17770784, 11797796, 31950843, 13929123, -25888302, 12288344, -30341101, -7336386, 13847711, 5387222 }, + }, + { + { -18582163, -3416217, 17824843, -2340966, 22744343, -10442611, 8763061, 3617786, -19600662, 10370991 }, + { 20246567, -14369378, 22358229, -543712, 18507283, -10413996, 14554437, -8746092, 32232924, 16763880 }, + { 9648505, 10094563, 26416693, 14745928, -30374318, -6472621, 11094161, 15689506, 3140038, -16510092 }, + }, + { + { -16160072, 5472695, 31895588, 4744994, 8823515, 10365685, -27224800, 9448613, -28774454, 366295 }, + { 19153450, 11523972, -11096490, -6503142, -24647631, 5420647, 28344573, 8041113, 719605, 11671788 }, + { 8678025, 2694440, -6808014, 2517372, 4964326, 11152271, -15432916, -15266516, 27000813, -10195553 }, + }, + { + { -15157904, 7134312, 8639287, -2814877, -7235688, 10421742, 564065, 5336097, 6750977, -14521026 }, + { 11836410, -3979488, 26297894, 16080799, 23455045, 15735944, 1695823, -8819122, 8169720, 16220347 }, + { -18115838, 8653647, 17578566, -6092619, -8025777, -16012763, -11144307, -2627664, -5990708, -14166033 }, + }, + { + { -23308498, -10968312, 15213228, -10081214, -30853605, -11050004, 27884329, 2847284, 2655861, 1738395 }, + { -27537433, -14253021, -25336301, -8002780, -9370762, 8129821, 21651608, -3239336, -19087449, -11005278 }, + { 1533110, 3437855, 23735889, 459276, 29970501, 11335377, 26030092, 5821408, 10478196, 8544890 }, + }, + { + { 32173121, -16129311, 24896207, 3921497, 22579056, -3410854, 19270449, 12217473, 17789017, -3395995 }, + { -30552961, -2228401, -15578829, -10147201, 13243889, 517024, 15479401, -3853233, 30460520, 1052596 }, + { -11614875, 13323618, 32618793, 8175907, -15230173, 12596687, 27491595, -4612359, 3179268, -9478891 }, + }, + { + { 31947069, -14366651, -4640583, -15339921, -15125977, -6039709, -14756777, -16411740, 19072640, -9511060 }, + { 11685058, 11822410, 3158003, -13952594, 33402194, -4165066, 5977896, -5215017, 473099, 5040608 }, + { -20290863, 8198642, -27410132, 11602123, 1290375, -2799760, 28326862, 1721092, -19558642, -3131606 }, + }, + }, + { + { + { 7881532, 10687937, 7578723, 7738378, -18951012, -2553952, 21820786, 8076149, -27868496, 11538389 }, + { -19935666, 3899861, 18283497, -6801568, -15728660, -11249211, 8754525, 7446702, -5676054, 5797016 }, + { -11295600, -3793569, -15782110, -7964573, 12708869, -8456199, 2014099, -9050574, -2369172, -5877341 }, + }, + { + { -22472376, -11568741, -27682020, 1146375, 18956691, 16640559, 1192730, -3714199, 15123619, 10811505 }, + { 14352098, -3419715, -18942044, 10822655, 32750596, 4699007, -70363, 15776356, -28886779, -11974553 }, + { -28241164, -8072475, -4978962, -5315317, 29416931, 1847569, -20654173, -16484855, 4714547, -9600655 }, + }, + { + { 15200332, 8368572, 19679101, 15970074, -31872674, 1959451, 24611599, -4543832, -11745876, 12340220 }, + { 12876937, -10480056, 33134381, 6590940, -6307776, 14872440, 9613953, 8241152, 15370987, 9608631 }, + { -4143277, -12014408, 8446281, -391603, 4407738, 13629032, -7724868, 15866074, -28210621, -8814099 }, + }, + { + { 26660628, -15677655, 8393734, 358047, -7401291, 992988, -23904233, 858697, 20571223, 8420556 }, + { 14620715, 13067227, -15447274, 8264467, 14106269, 15080814, 33531827, 12516406, -21574435, -12476749 }, + { 236881, 10476226, 57258, -14677024, 6472998, 2466984, 17258519, 7256740, 8791136, 15069930 }, + }, + { + { 1276410, -9371918, 22949635, -16322807, -23493039, -5702186, 14711875, 4874229, -30663140, -2331391 }, + { 5855666, 4990204, -13711848, 7294284, -7804282, 1924647, -1423175, -7912378, -33069337, 9234253 }, + { 20590503, -9018988, 31529744, -7352666, -2706834, 10650548, 31559055, -11609587, 18979186, 13396066 }, + }, + { + { 24474287, 4968103, 22267082, 4407354, 24063882, -8325180, -18816887, 13594782, 33514650, 7021958 }, + { -11566906, -6565505, -21365085, 15928892, -26158305, 4315421, -25948728, -3916677, -21480480, 12868082 }, + { -28635013, 13504661, 19988037, -2132761, 21078225, 6443208, -21446107, 2244500, -12455797, -8089383 }, + }, + { + { -30595528, 13793479, -5852820, 319136, -25723172, -6263899, 33086546, 8957937, -15233648, 5540521 }, + { -11630176, -11503902, -8119500, -7643073, 2620056, 1022908, -23710744, -1568984, -16128528, -14962807 }, + { 23152971, 775386, 27395463, 14006635, -9701118, 4649512, 1689819, 892185, -11513277, -15205948 }, + }, + { + { 9770129, 9586738, 26496094, 4324120, 1556511, -3550024, 27453819, 4763127, -19179614, 5867134 }, + { -32765025, 1927590, 31726409, -4753295, 23962434, -16019500, 27846559, 5931263, -29749703, -16108455 }, + { 27461885, -2977536, 22380810, 1815854, -23033753, -3031938, 7283490, -15148073, -19526700, 7734629 }, + }, + }, + { + { + { -8010264, -9590817, -11120403, 6196038, 29344158, -13430885, 7585295, -3176626, 18549497, 15302069 }, + { -32658337, -6171222, -7672793, -11051681, 6258878, 13504381, 10458790, -6418461, -8872242, 8424746 }, + { 24687205, 8613276, -30667046, -3233545, 1863892, -1830544, 19206234, 7134917, -11284482, -828919 }, + }, + { + { 11334899, -9218022, 8025293, 12707519, 17523892, -10476071, 10243738, -14685461, -5066034, 16498837 }, + { 8911542, 6887158, -9584260, -6958590, 11145641, -9543680, 17303925, -14124238, 6536641, 10543906 }, + { -28946384, 15479763, -17466835, 568876, -1497683, 11223454, -2669190, -16625574, -27235709, 8876771 }, + }, + { + { -25742899, -12566864, -15649966, -846607, -33026686, -796288, -33481822, 15824474, -604426, -9039817 }, + { 10330056, 70051, 7957388, -9002667, 9764902, 15609756, 27698697, -4890037, 1657394, 3084098 }, + { 10477963, -7470260, 12119566, -13250805, 29016247, -5365589, 31280319, 14396151, -30233575, 15272409 }, + }, + { + { -12288309, 3169463, 28813183, 16658753, 25116432, -5630466, -25173957, -12636138, -25014757, 1950504 }, + { -26180358, 9489187, 11053416, -14746161, -31053720, 5825630, -8384306, -8767532, 15341279, 8373727 }, + { 28685821, 7759505, -14378516, -12002860, -31971820, 4079242, 298136, -10232602, -2878207, 15190420 }, + }, + { + { -32932876, 13806336, -14337485, -15794431, -24004620, 10940928, 8669718, 2742393, -26033313, -6875003 }, + { -1580388, -11729417, -25979658, -11445023, -17411874, -10912854, 9291594, -16247779, -12154742, 6048605 }, + { -30305315, 14843444, 1539301, 11864366, 20201677, 1900163, 13934231, 5128323, 11213262, 9168384 }, + }, + { + { -26280513, 11007847, 19408960, -940758, -18592965, -4328580, -5088060, -11105150, 20470157, -16398701 }, + { -23136053, 9282192, 14855179, -15390078, -7362815, -14408560, -22783952, 14461608, 14042978, 5230683 }, + { 29969567, -2741594, -16711867, -8552442, 9175486, -2468974, 21556951, 3506042, -5933891, -12449708 }, + }, + { + { -3144746, 8744661, 19704003, 4581278, -20430686, 6830683, -21284170, 8971513, -28539189, 15326563 }, + { -19464629, 10110288, -17262528, -3503892, -23500387, 1355669, -15523050, 15300988, -20514118, 9168260 }, + { -5353335, 4488613, -23803248, 16314347, 7780487, -15638939, -28948358, 9601605, 33087103, -9011387 }, + }, + { + { -19443170, -15512900, -20797467, -12445323, -29824447, 10229461, -27444329, -15000531, -5996870, 15664672 }, + { 23294591, -16632613, -22650781, -8470978, 27844204, 11461195, 13099750, -2460356, 18151676, 13417686 }, + { -24722913, -4176517, -31150679, 5988919, -26858785, 6685065, 1661597, -12551441, 15271676, -15452665 }, + }, + }, + { + { + { 11433042, -13228665, 8239631, -5279517, -1985436, -725718, -18698764, 2167544, -6921301, -13440182 }, + { -31436171, 15575146, 30436815, 12192228, -22463353, 9395379, -9917708, -8638997, 12215110, 12028277 }, + { 14098400, 6555944, 23007258, 5757252, -15427832, -12950502, 30123440, 4617780, -16900089, -655628 }, + }, + { + { -4026201, -15240835, 11893168, 13718664, -14809462, 1847385, -15819999, 10154009, 23973261, -12684474 }, + { -26531820, -3695990, -1908898, 2534301, -31870557, -16550355, 18341390, -11419951, 32013174, -10103539 }, + { -25479301, 10876443, -11771086, -14625140, -12369567, 1838104, 21911214, 6354752, 4425632, -837822 }, + }, + { + { -10433389, -14612966, 22229858, -3091047, -13191166, 776729, -17415375, -12020462, 4725005, 14044970 }, + { 19268650, -7304421, 1555349, 8692754, -21474059, -9910664, 6347390, -1411784, -19522291, -16109756 }, + { -24864089, 12986008, -10898878, -5558584, -11312371, -148526, 19541418, 8180106, 9282262, 10282508 }, + }, + { + { -26205082, 4428547, -8661196, -13194263, 4098402, -14165257, 15522535, 8372215, 5542595, -10702683 }, + { -10562541, 14895633, 26814552, -16673850, -17480754, -2489360, -2781891, 6993761, -18093885, 10114655 }, + { -20107055, -929418, 31422704, 10427861, -7110749, 6150669, -29091755, -11529146, 25953725, -106158 }, + }, + { + { -4234397, -8039292, -9119125, 3046000, 2101609, -12607294, 19390020, 6094296, -3315279, 12831125 }, + { -15998678, 7578152, 5310217, 14408357, -33548620, -224739, 31575954, 6326196, 7381791, -2421839 }, + { -20902779, 3296811, 24736065, -16328389, 18374254, 7318640, 6295303, 8082724, -15362489, 12339664 }, + }, + { + { 27724736, 2291157, 6088201, -14184798, 1792727, 5857634, 13848414, 15768922, 25091167, 14856294 }, + { -18866652, 8331043, 24373479, 8541013, -701998, -9269457, 12927300, -12695493, -22182473, -9012899 }, + { -11423429, -5421590, 11632845, 3405020, 30536730, -11674039, -27260765, 13866390, 30146206, 9142070 }, + }, + { + { 3924129, -15307516, -13817122, -10054960, 12291820, -668366, -27702774, 9326384, -8237858, 4171294 }, + { -15921940, 16037937, 6713787, 16606682, -21612135, 2790944, 26396185, 3731949, 345228, -5462949 }, + { -21327538, 13448259, 25284571, 1143661, 20614966, -8849387, 2031539, -12391231, -16253183, -13582083 }, + }, + { + { 31016211, -16722429, 26371392, -14451233, -5027349, 14854137, 17477601, 3842657, 28012650, -16405420 }, + { -5075835, 9368966, -8562079, -4600902, -15249953, 6970560, -9189873, 16292057, -8867157, 3507940 }, + { 29439664, 3537914, 23333589, 6997794, -17555561, -11018068, -15209202, -15051267, -9164929, 6580396 }, + }, + }, + { + { + { -12185861, -7679788, 16438269, 10826160, -8696817, -6235611, 17860444, -9273846, -2095802, 9304567 }, + { 20714564, -4336911, 29088195, 7406487, 11426967, -5095705, 14792667, -14608617, 5289421, -477127 }, + { -16665533, -10650790, -6160345, -13305760, 9192020, -1802462, 17271490, 12349094, 26939669, -3752294 }, + }, + { + { -12889898, 9373458, 31595848, 16374215, 21471720, 13221525, -27283495, -12348559, -3698806, 117887 }, + { 22263325, -6560050, 3984570, -11174646, -15114008, -566785, 28311253, 5358056, -23319780, 541964 }, + { 16259219, 3261970, 2309254, -15534474, -16885711, -4581916, 24134070, -16705829, -13337066, -13552195 }, + }, + { + { 9378160, -13140186, -22845982, -12745264, 28198281, -7244098, -2399684, -717351, 690426, 14876244 }, + { 24977353, -314384, -8223969, -13465086, 28432343, -1176353, -13068804, -12297348, -22380984, 6618999 }, + { -1538174, 11685646, 12944378, 13682314, -24389511, -14413193, 8044829, -13817328, 32239829, -5652762 }, + }, + { + { -18603066, 4762990, -926250, 8885304, -28412480, -3187315, 9781647, -10350059, 32779359, 5095274 }, + { -33008130, -5214506, -32264887, -3685216, 9460461, -9327423, -24601656, 14506724, 21639561, -2630236 }, + { -16400943, -13112215, 25239338, 15531969, 3987758, -4499318, -1289502, -6863535, 17874574, 558605 }, + }, + { + { -13600129, 10240081, 9171883, 16131053, -20869254, 9599700, 33499487, 5080151, 2085892, 5119761 }, + { -22205145, -2519528, -16381601, 414691, -25019550, 2170430, 30634760, -8363614, -31999993, -5759884 }, + { -6845704, 15791202, 8550074, -1312654, 29928809, -12092256, 27534430, -7192145, -22351378, 12961482 }, + }, + { + { -24492060, -9570771, 10368194, 11582341, -23397293, -2245287, 16533930, 8206996, -30194652, -5159638 }, + { -11121496, -3382234, 2307366, 6362031, -135455, 8868177, -16835630, 7031275, 7589640, 8945490 }, + { -32152748, 8917967, 6661220, -11677616, -1192060, -15793393, 7251489, -11182180, 24099109, -14456170 }, + }, + { + { 5019558, -7907470, 4244127, -14714356, -26933272, 6453165, -19118182, -13289025, -6231896, -10280736 }, + { 10853594, 10721687, 26480089, 5861829, -22995819, 1972175, -1866647, -10557898, -3363451, -6441124 }, + { -17002408, 5906790, 221599, -6563147, 7828208, -13248918, 24362661, -2008168, -13866408, 7421392 }, + }, + { + { 8139927, -6546497, 32257646, -5890546, 30375719, 1886181, -21175108, 15441252, 28826358, -4123029 }, + { 6267086, 9695052, 7709135, -16603597, -32869068, -1886135, 14795160, -7840124, 13746021, -1742048 }, + { 28584902, 7787108, -6732942, -15050729, 22846041, -7571236, -3181936, -363524, 4771362, -8419958 }, + }, + }, + { + { + { 24949256, 6376279, -27466481, -8174608, -18646154, -9930606, 33543569, -12141695, 3569627, 11342593 }, + { 26514989, 4740088, 27912651, 3697550, 19331575, -11472339, 6809886, 4608608, 7325975, -14801071 }, + { -11618399, -14554430, -24321212, 7655128, -1369274, 5214312, -27400540, 10258390, -17646694, -8186692 }, + }, + { + { 11431204, 15823007, 26570245, 14329124, 18029990, 4796082, -31446179, 15580664, 9280358, -3973687 }, + { -160783, -10326257, -22855316, -4304997, -20861367, -13621002, -32810901, -11181622, -15545091, 4387441 }, + { -20799378, 12194512, 3937617, -5805892, -27154820, 9340370, -24513992, 8548137, 20617071, -7482001 }, + }, + { + { -938825, -3930586, -8714311, 16124718, 24603125, -6225393, -13775352, -11875822, 24345683, 10325460 }, + { -19855277, -1568885, -22202708, 8714034, 14007766, 6928528, 16318175, -1010689, 4766743, 3552007 }, + { -21751364, -16730916, 1351763, -803421, -4009670, 3950935, 3217514, 14481909, 10988822, -3994762 }, + }, + { + { 15564307, -14311570, 3101243, 5684148, 30446780, -8051356, 12677127, -6505343, -8295852, 13296005 }, + { -9442290, 6624296, -30298964, -11913677, -4670981, -2057379, 31521204, 9614054, -30000824, 12074674 }, + { 4771191, -135239, 14290749, -13089852, 27992298, 14998318, -1413936, -1556716, 29832613, -16391035 }, + }, + { + { 7064884, -7541174, -19161962, -5067537, -18891269, -2912736, 25825242, 5293297, -27122660, 13101590 }, + { -2298563, 2439670, -7466610, 1719965, -27267541, -16328445, 32512469, -5317593, -30356070, -4190957 }, + { -30006540, 10162316, -33180176, 3981723, -16482138, -13070044, 14413974, 9515896, 19568978, 9628812 }, + }, + { + { 33053803, 199357, 15894591, 1583059, 27380243, -4580435, -17838894, -6106839, -6291786, 3437740 }, + { -18978877, 3884493, 19469877, 12726490, 15913552, 13614290, -22961733, 70104, 7463304, 4176122 }, + { -27124001, 10659917, 11482427, -16070381, 12771467, -6635117, -32719404, -5322751, 24216882, 5944158 }, + }, + { + { 8894125, 7450974, -2664149, -9765752, -28080517, -12389115, 19345746, 14680796, 11632993, 5847885 }, + { 26942781, -2315317, 9129564, -4906607, 26024105, 11769399, -11518837, 6367194, -9727230, 4782140 }, + { 19916461, -4828410, -22910704, -11414391, 25606324, -5972441, 33253853, 8220911, 6358847, -1873857 }, + }, + { + { 801428, -2081702, 16569428, 11065167, 29875704, 96627, 7908388, -4480480, -13538503, 1387155 }, + { 19646058, 5720633, -11416706, 12814209, 11607948, 12749789, 14147075, 15156355, -21866831, 11835260 }, + { 19299512, 1155910, 28703737, 14890794, 2925026, 7269399, 26121523, 15467869, -26560550, 5052483 }, + }, + }, + { + { + { -3017432, 10058206, 1980837, 3964243, 22160966, 12322533, -6431123, -12618185, 12228557, -7003677 }, + { 32944382, 14922211, -22844894, 5188528, 21913450, -8719943, 4001465, 13238564, -6114803, 8653815 }, + { 22865569, -4652735, 27603668, -12545395, 14348958, 8234005, 24808405, 5719875, 28483275, 2841751 }, + }, + { + { -16420968, -1113305, -327719, -12107856, 21886282, -15552774, -1887966, -315658, 19932058, -12739203 }, + { -11656086, 10087521, -8864888, -5536143, -19278573, -3055912, 3999228, 13239134, -4777469, -13910208 }, + { 1382174, -11694719, 17266790, 9194690, -13324356, 9720081, 20403944, 11284705, -14013818, 3093230 }, + }, + { + { 16650921, -11037932, -1064178, 1570629, -8329746, 7352753, -302424, 16271225, -24049421, -6691850 }, + { -21911077, -5927941, -4611316, -5560156, -31744103, -10785293, 24123614, 15193618, -21652117, -16739389 }, + { -9935934, -4289447, -25279823, 4372842, 2087473, 10399484, 31870908, 14690798, 17361620, 11864968 }, + }, + { + { -11307610, 6210372, 13206574, 5806320, -29017692, -13967200, -12331205, -7486601, -25578460, -16240689 }, + { 14668462, -12270235, 26039039, 15305210, 25515617, 4542480, 10453892, 6577524, 9145645, -6443880 }, + { 5974874, 3053895, -9433049, -10385191, -31865124, 3225009, -7972642, 3936128, -5652273, -3050304 }, + }, + { + { 30625386, -4729400, -25555961, -12792866, -20484575, 7695099, 17097188, -16303496, -27999779, 1803632 }, + { -3553091, 9865099, -5228566, 4272701, -5673832, -16689700, 14911344, 12196514, -21405489, 7047412 }, + { 20093277, 9920966, -11138194, -5343857, 13161587, 12044805, -32856851, 4124601, -32343828, -10257566 }, + }, + { + { -20788824, 14084654, -13531713, 7842147, 19119038, -13822605, 4752377, -8714640, -21679658, 2288038 }, + { -26819236, -3283715, 29965059, 3039786, -14473765, 2540457, 29457502, 14625692, -24819617, 12570232 }, + { -1063558, -11551823, 16920318, 12494842, 1278292, -5869109, -21159943, -3498680, -11974704, 4724943 }, + }, + { + { 17960970, -11775534, -4140968, -9702530, -8876562, -1410617, -12907383, -8659932, -29576300, 1903856 }, + { 23134274, -14279132, -10681997, -1611936, 20684485, 15770816, -12989750, 3190296, 26955097, 14109738 }, + { 15308788, 5320727, -30113809, -14318877, 22902008, 7767164, 29425325, -11277562, 31960942, 11934971 }, + }, + { + { -27395711, 8435796, 4109644, 12222639, -24627868, 14818669, 20638173, 4875028, 10491392, 1379718 }, + { -13159415, 9197841, 3875503, -8936108, -1383712, -5879801, 33518459, 16176658, 21432314, 12180697 }, + { -11787308, 11500838, 13787581, -13832590, -22430679, 10140205, 1465425, 12689540, -10301319, -13872883 }, + }, + }, + { + { + { 5414091, -15386041, -21007664, 9643570, 12834970, 1186149, -2622916, -1342231, 26128231, 6032912 }, + { -26337395, -13766162, 32496025, -13653919, 17847801, -12669156, 3604025, 8316894, -25875034, -10437358 }, + { 3296484, 6223048, 24680646, -12246460, -23052020, 5903205, -8862297, -4639164, 12376617, 3188849 }, + }, + { + { 29190488, -14659046, 27549113, -1183516, 3520066, -10697301, 32049515, -7309113, -16109234, -9852307 }, + { -14744486, -9309156, 735818, -598978, -20407687, -5057904, 25246078, -15795669, 18640741, -960977 }, + { -6928835, -16430795, 10361374, 5642961, 4910474, 12345252, -31638386, -494430, 10530747, 1053335 }, + }, + { + { -29265967, -14186805, -13538216, -12117373, -19457059, -10655384, -31462369, -2948985, 24018831, 15026644 }, + { -22592535, -3145277, -2289276, 5953843, -13440189, 9425631, 25310643, 13003497, -2314791, -15145616 }, + { -27419985, -603321, -8043984, -1669117, -26092265, 13987819, -27297622, 187899, -23166419, -2531735 }, + }, + { + { -21744398, -13810475, 1844840, 5021428, -10434399, -15911473, 9716667, 16266922, -5070217, 726099 }, + { 29370922, -6053998, 7334071, -15342259, 9385287, 2247707, -13661962, -4839461, 30007388, -15823341 }, + { -936379, 16086691, 23751945, -543318, -1167538, -5189036, 9137109, 730663, 9835848, 4555336 }, + }, + { + { -23376435, 1410446, -22253753, -12899614, 30867635, 15826977, 17693930, 544696, -11985298, 12422646 }, + { 31117226, -12215734, -13502838, 6561947, -9876867, -12757670, -5118685, -4096706, 29120153, 13924425 }, + { -17400879, -14233209, 19675799, -2734756, -11006962, -5858820, -9383939, -11317700, 7240931, -237388 }, + }, + { + { -31361739, -11346780, -15007447, -5856218, -22453340, -12152771, 1222336, 4389483, 3293637, -15551743 }, + { -16684801, -14444245, 11038544, 11054958, -13801175, -3338533, -24319580, 7733547, 12796905, -6335822 }, + { -8759414, -10817836, -25418864, 10783769, -30615557, -9746811, -28253339, 3647836, 3222231, -11160462 }, + }, + { + { 18606113, 1693100, -25448386, -15170272, 4112353, 10045021, 23603893, -2048234, -7550776, 2484985 }, + { 9255317, -3131197, -12156162, -1004256, 13098013, -9214866, 16377220, -2102812, -19802075, -3034702 }, + { -22729289, 7496160, -5742199, 11329249, 19991973, -3347502, -31718148, 9936966, -30097688, -10618797 }, + }, + { + { 21878590, -5001297, 4338336, 13643897, -3036865, 13160960, 19708896, 5415497, -7360503, -4109293 }, + { 27736861, 10103576, 12500508, 8502413, -3413016, -9633558, 10436918, -1550276, -23659143, -8132100 }, + { 19492550, -12104365, -29681976, -852630, -3208171, 12403437, 30066266, 8367329, 13243957, 8709688 }, + }, + }, + { + { + { 12015105, 2801261, 28198131, 10151021, 24818120, -4743133, -11194191, -5645734, 5150968, 7274186 }, + { 2831366, -12492146, 1478975, 6122054, 23825128, -12733586, 31097299, 6083058, 31021603, -9793610 }, + { -2529932, -2229646, 445613, 10720828, -13849527, -11505937, -23507731, 16354465, 15067285, -14147707 }, + }, + { + { 7840942, 14037873, -33364863, 15934016, -728213, -3642706, 21403988, 1057586, -19379462, -12403220 }, + { 915865, -16469274, 15608285, -8789130, -24357026, 6060030, -17371319, 8410997, -7220461, 16527025 }, + { 32922597, -556987, 20336074, -16184568, 10903705, -5384487, 16957574, 52992, 23834301, 6588044 }, + }, + { + { 32752030, 11232950, 3381995, -8714866, 22652988, -10744103, 17159699, 16689107, -20314580, -1305992 }, + { -4689649, 9166776, -25710296, -10847306, 11576752, 12733943, 7924251, -2752281, 1976123, -7249027 }, + { 21251222, 16309901, -2983015, -6783122, 30810597, 12967303, 156041, -3371252, 12331345, -8237197 }, + }, + { + { 8651614, -4477032, -16085636, -4996994, 13002507, 2950805, 29054427, -5106970, 10008136, -4667901 }, + { 31486080, 15114593, -14261250, 12951354, 14369431, -7387845, 16347321, -13662089, 8684155, -10532952 }, + { 19443825, 11385320, 24468943, -9659068, -23919258, 2187569, -26263207, -6086921, 31316348, 14219878 }, + }, + { + { -28594490, 1193785, 32245219, 11392485, 31092169, 15722801, 27146014, 6992409, 29126555, 9207390 }, + { 32382935, 1110093, 18477781, 11028262, -27411763, -7548111, -4980517, 10843782, -7957600, -14435730 }, + { 2814918, 7836403, 27519878, -7868156, -20894015, -11553689, -21494559, 8550130, 28346258, 1994730 }, + }, + { + { -19578299, 8085545, -14000519, -3948622, 2785838, -16231307, -19516951, 7174894, 22628102, 8115180 }, + { -30405132, 955511, -11133838, -15078069, -32447087, -13278079, -25651578, 3317160, -9943017, 930272 }, + { -15303681, -6833769, 28856490, 1357446, 23421993, 1057177, 24091212, -1388970, -22765376, -10650715 }, + }, + { + { -22751231, -5303997, -12907607, -12768866, -15811511, -7797053, -14839018, -16554220, -1867018, 8398970 }, + { -31969310, 2106403, -4736360, 1362501, 12813763, 16200670, 22981545, -6291273, 18009408, -15772772 }, + { -17220923, -9545221, -27784654, 14166835, 29815394, 7444469, 29551787, -3727419, 19288549, 1325865 }, + }, + { + { 15100157, -15835752, -23923978, -1005098, -26450192, 15509408, 12376730, -3479146, 33166107, -8042750 }, + { 20909231, 13023121, -9209752, 16251778, -5778415, -8094914, 12412151, 10018715, 2213263, -13878373 }, + { 32529814, -11074689, 30361439, -16689753, -9135940, 1513226, 22922121, 6382134, -5766928, 8371348 }, + }, + }, + { + { + { 9923462, 11271500, 12616794, 3544722, -29998368, -1721626, 12891687, -8193132, -26442943, 10486144 }, + { -22597207, -7012665, 8587003, -8257861, 4084309, -12970062, 361726, 2610596, -23921530, -11455195 }, + { 5408411, -1136691, -4969122, 10561668, 24145918, 14240566, 31319731, -4235541, 19985175, -3436086 }, + }, + { + { -13994457, 16616821, 14549246, 3341099, 32155958, 13648976, -17577068, 8849297, 65030, 8370684 }, + { -8320926, -12049626, 31204563, 5839400, -20627288, -1057277, -19442942, 6922164, 12743482, -9800518 }, + { -2361371, 12678785, 28815050, 4759974, -23893047, 4884717, 23783145, 11038569, 18800704, 255233 }, + }, + { + { -5269658, -1773886, 13957886, 7990715, 23132995, 728773, 13393847, 9066957, 19258688, -14753793 }, + { -2936654, -10827535, -10432089, 14516793, -3640786, 4372541, -31934921, 2209390, -1524053, 2055794 }, + { 580882, 16705327, 5468415, -2683018, -30926419, -14696000, -7203346, -8994389, -30021019, 7394435 }, + }, + { + { 23838809, 1822728, -15738443, 15242727, 8318092, -3733104, -21672180, -3492205, -4821741, 14799921 }, + { 13345610, 9759151, 3371034, -16137791, 16353039, 8577942, 31129804, 13496856, -9056018, 7402518 }, + { 2286874, -4435931, -20042458, -2008336, -13696227, 5038122, 11006906, -15760352, 8205061, 1607563 }, + }, + { + { 14414086, -8002132, 3331830, -3208217, 22249151, -5594188, 18364661, -2906958, 30019587, -9029278 }, + { -27688051, 1585953, -10775053, 931069, -29120221, -11002319, -14410829, 12029093, 9944378, 8024 }, + { 4368715, -3709630, 29874200, -15022983, -20230386, -11410704, -16114594, -999085, -8142388, 5640030 }, + }, + { + { 10299610, 13746483, 11661824, 16234854, 7630238, 5998374, 9809887, -16694564, 15219798, -14327783 }, + { 27425505, -5719081, 3055006, 10660664, 23458024, 595578, -15398605, -1173195, -18342183, 9742717 }, + { 6744077, 2427284, 26042789, 2720740, -847906, 1118974, 32324614, 7406442, 12420155, 1994844 }, + }, + { + { 14012521, -5024720, -18384453, -9578469, -26485342, -3936439, -13033478, -10909803, 24319929, -6446333 }, + { 16412690, -4507367, 10772641, 15929391, -17068788, -4658621, 10555945, -10484049, -30102368, -4739048 }, + { 22397382, -7767684, -9293161, -12792868, 17166287, -9755136, -27333065, 6199366, 21880021, -12250760 }, + }, + { + { -4283307, 5368523, -31117018, 8163389, -30323063, 3209128, 16557151, 8890729, 8840445, 4957760 }, + { -15447727, 709327, -6919446, -10870178, -29777922, 6522332, -21720181, 12130072, -14796503, 5005757 }, + { -2114751, -14308128, 23019042, 15765735, -25269683, 6002752, 10183197, -13239326, -16395286, -2176112 }, + }, + }, + { + { + { -19025756, 1632005, 13466291, -7995100, -23640451, 16573537, -32013908, -3057104, 22208662, 2000468 }, + { 3065073, -1412761, -25598674, -361432, -17683065, -5703415, -8164212, 11248527, -3691214, -7414184 }, + { 10379208, -6045554, 8877319, 1473647, -29291284, -12507580, 16690915, 2553332, -3132688, 16400289 }, + }, + { + { 15716668, 1254266, -18472690, 7446274, -8448918, 6344164, -22097271, -7285580, 26894937, 9132066 }, + { 24158887, 12938817, 11085297, -8177598, -28063478, -4457083, -30576463, 64452, -6817084, -2692882 }, + { 13488534, 7794716, 22236231, 5989356, 25426474, -12578208, 2350710, -3418511, -4688006, 2364226 }, + }, + { + { 16335052, 9132434, 25640582, 6678888, 1725628, 8517937, -11807024, -11697457, 15445875, -7798101 }, + { 29004207, -7867081, 28661402, -640412, -12794003, -7943086, 31863255, -4135540, -278050, -15759279 }, + { -6122061, -14866665, -28614905, 14569919, -10857999, -3591829, 10343412, -6976290, -29828287, -10815811 }, + }, + { + { 27081650, 3463984, 14099042, -4517604, 1616303, -6205604, 29542636, 15372179, 17293797, 960709 }, + { 20263915, 11434237, -5765435, 11236810, 13505955, -10857102, -16111345, 6493122, -19384511, 7639714 }, + { -2830798, -14839232, 25403038, -8215196, -8317012, -16173699, 18006287, -16043750, 29994677, -15808121 }, + }, + { + { 9769828, 5202651, -24157398, -13631392, -28051003, -11561624, -24613141, -13860782, -31184575, 709464 }, + { 12286395, 13076066, -21775189, -1176622, -25003198, 4057652, -32018128, -8890874, 16102007, 13205847 }, + { 13733362, 5599946, 10557076, 3195751, -5557991, 8536970, -25540170, 8525972, 10151379, 10394400 }, + }, + { + { 4024660, -16137551, 22436262, 12276534, -9099015, -2686099, 19698229, 11743039, -33302334, 8934414 }, + { -15879800, -4525240, -8580747, -2934061, 14634845, -698278, -9449077, 3137094, -11536886, 11721158 }, + { 17555939, -5013938, 8268606, 2331751, -22738815, 9761013, 9319229, 8835153, -9205489, -1280045 }, + }, + { + { -461409, -7830014, 20614118, 16688288, -7514766, -4807119, 22300304, 505429, 6108462, -6183415 }, + { -5070281, 12367917, -30663534, 3234473, 32617080, -8422642, 29880583, -13483331, -26898490, -7867459 }, + { -31975283, 5726539, 26934134, 10237677, -3173717, -605053, 24199304, 3795095, 7592688, -14992079 }, + }, + { + { 21594432, -14964228, 17466408, -4077222, 32537084, 2739898, 6407723, 12018833, -28256052, 4298412 }, + { -20650503, -11961496, -27236275, 570498, 3767144, -1717540, 13891942, -1569194, 13717174, 10805743 }, + { -14676630, -15644296, 15287174, 11927123, 24177847, -8175568, -796431, 14860609, -26938930, -5863836 }, + }, + }, + { + { + { 12962541, 5311799, -10060768, 11658280, 18855286, -7954201, 13286263, -12808704, -4381056, 9882022 }, + { 18512079, 11319350, -20123124, 15090309, 18818594, 5271736, -22727904, 3666879, -23967430, -3299429 }, + { -6789020, -3146043, 16192429, 13241070, 15898607, -14206114, -10084880, -6661110, -2403099, 5276065 }, + }, + { + { 30169808, -5317648, 26306206, -11750859, 27814964, 7069267, 7152851, 3684982, 1449224, 13082861 }, + { 10342826, 3098505, 2119311, 193222, 25702612, 12233820, 23697382, 15056736, -21016438, -8202000 }, + { -33150110, 3261608, 22745853, 7948688, 19370557, -15177665, -26171976, 6482814, -10300080, -11060101 }, + }, + { + { 32869458, -5408545, 25609743, 15678670, -10687769, -15471071, 26112421, 2521008, -22664288, 6904815 }, + { 29506923, 4457497, 3377935, -9796444, -30510046, 12935080, 1561737, 3841096, -29003639, -6657642 }, + { 10340844, -6630377, -18656632, -2278430, 12621151, -13339055, 30878497, -11824370, -25584551, 5181966 }, + }, + { + { 25940115, -12658025, 17324188, -10307374, -8671468, 15029094, 24396252, -16450922, -2322852, -12388574 }, + { -21765684, 9916823, -1300409, 4079498, -1028346, 11909559, 1782390, 12641087, 20603771, -6561742 }, + { -18882287, -11673380, 24849422, 11501709, 13161720, -4768874, 1925523, 11914390, 4662781, 7820689 }, + }, + { + { 12241050, -425982, 8132691, 9393934, 32846760, -1599620, 29749456, 12172924, 16136752, 15264020 }, + { -10349955, -14680563, -8211979, 2330220, -17662549, -14545780, 10658213, 6671822, 19012087, 3772772 }, + { 3753511, -3421066, 10617074, 2028709, 14841030, -6721664, 28718732, -15762884, 20527771, 12988982 }, + }, + { + { -14822485, -5797269, -3707987, 12689773, -898983, -10914866, -24183046, -10564943, 3299665, -12424953 }, + { -16777703, -15253301, -9642417, 4978983, 3308785, 8755439, 6943197, 6461331, -25583147, 8991218 }, + { -17226263, 1816362, -1673288, -6086439, 31783888, -8175991, -32948145, 7417950, -30242287, 1507265 }, + }, + { + { 29692663, 6829891, -10498800, 4334896, 20945975, -11906496, -28887608, 8209391, 14606362, -10647073 }, + { -3481570, 8707081, 32188102, 5672294, 22096700, 1711240, -33020695, 9761487, 4170404, -2085325 }, + { -11587470, 14855945, -4127778, -1531857, -26649089, 15084046, 22186522, 16002000, -14276837, -8400798 }, + }, + { + { -4811456, 13761029, -31703877, -2483919, -3312471, 7869047, -7113572, -9620092, 13240845, 10965870 }, + { -7742563, -8256762, -14768334, -13656260, -23232383, 12387166, 4498947, 14147411, 29514390, 4302863 }, + { -13413405, -12407859, 20757302, -13801832, 14785143, 8976368, -5061276, -2144373, 17846988, -13971927 }, + }, + }, + { + { + { -2244452, -754728, -4597030, -1066309, -6247172, 1455299, -21647728, -9214789, -5222701, 12650267 }, + { -9906797, -16070310, 21134160, 12198166, -27064575, 708126, 387813, 13770293, -19134326, 10958663 }, + { 22470984, 12369526, 23446014, -5441109, -21520802, -9698723, -11772496, -11574455, -25083830, 4271862 }, + }, + { + { -25169565, -10053642, -19909332, 15361595, -5984358, 2159192, 75375, -4278529, -32526221, 8469673 }, + { 15854970, 4148314, -8893890, 7259002, 11666551, 13824734, -30531198, 2697372, 24154791, -9460943 }, + { 15446137, -15806644, 29759747, 14019369, 30811221, -9610191, -31582008, 12840104, 24913809, 9815020 }, + }, + { + { -4709286, -5614269, -31841498, -12288893, -14443537, 10799414, -9103676, 13438769, 18735128, 9466238 }, + { 11933045, 9281483, 5081055, -5183824, -2628162, -4905629, -7727821, -10896103, -22728655, 16199064 }, + { 14576810, 379472, -26786533, -8317236, -29426508, -10812974, -102766, 1876699, 30801119, 2164795 }, + }, + { + { 15995086, 3199873, 13672555, 13712240, -19378835, -4647646, -13081610, -15496269, -13492807, 1268052 }, + { -10290614, -3659039, -3286592, 10948818, 23037027, 3794475, -3470338, -12600221, -17055369, 3565904 }, + { 29210088, -9419337, -5919792, -4952785, 10834811, -13327726, -16512102, -10820713, -27162222, -14030531 }, + }, + { + { -13161890, 15508588, 16663704, -8156150, -28349942, 9019123, -29183421, -3769423, 2244111, -14001979 }, + { -5152875, -3800936, -9306475, -6071583, 16243069, 14684434, -25673088, -16180800, 13491506, 4641841 }, + { 10813417, 643330, -19188515, -728916, 30292062, -16600078, 27548447, -7721242, 14476989, -12767431 }, + }, + { + { 10292079, 9984945, 6481436, 8279905, -7251514, 7032743, 27282937, -1644259, -27912810, 12651324 }, + { -31185513, -813383, 22271204, 11835308, 10201545, 15351028, 17099662, 3988035, 21721536, -3148940 }, + { 10202177, -6545839, -31373232, -9574638, -32150642, -8119683, -12906320, 3852694, 13216206, 14842320 }, + }, + { + { -15815640, -10601066, -6538952, -7258995, -6984659, -6581778, -31500847, 13765824, -27434397, 9900184 }, + { 14465505, -13833331, -32133984, -14738873, -27443187, 12990492, 33046193, 15796406, -7051866, -8040114 }, + { 30924417, -8279620, 6359016, -12816335, 16508377, 9071735, -25488601, 15413635, 9524356, -7018878 }, + }, + { + { 12274201, -13175547, 32627641, -1785326, 6736625, 13267305, 5237659, -5109483, 15663516, 4035784 }, + { -2951309, 8903985, 17349946, 601635, -16432815, -4612556, -13732739, -15889334, -22258478, 4659091 }, + { -16916263, -4952973, -30393711, -15158821, 20774812, 15897498, 5736189, 15026997, -2178256, -13455585 }, + }, + }, + { + { + { -8858980, -2219056, 28571666, -10155518, -474467, -10105698, -3801496, 278095, 23440562, -290208 }, + { 10226241, -5928702, 15139956, 120818, -14867693, 5218603, 32937275, 11551483, -16571960, -7442864 }, + { 17932739, -12437276, -24039557, 10749060, 11316803, 7535897, 22503767, 5561594, -3646624, 3898661 }, + }, + { + { 7749907, -969567, -16339731, -16464, -25018111, 15122143, -1573531, 7152530, 21831162, 1245233 }, + { 26958459, -14658026, 4314586, 8346991, -5677764, 11960072, -32589295, -620035, -30402091, -16716212 }, + { -12165896, 9166947, 33491384, 13673479, 29787085, 13096535, 6280834, 14587357, -22338025, 13987525 }, + }, + { + { -24349909, 7778775, 21116000, 15572597, -4833266, -5357778, -4300898, -5124639, -7469781, -2858068 }, + { 9681908, -6737123, -31951644, 13591838, -6883821, 386950, 31622781, 6439245, -14581012, 4091397 }, + { -8426427, 1470727, -28109679, -1596990, 3978627, -5123623, -19622683, 12092163, 29077877, -14741988 }, + }, + { + { 5269168, -6859726, -13230211, -8020715, 25932563, 1763552, -5606110, -5505881, -20017847, 2357889 }, + { 32264008, -15407652, -5387735, -1160093, -2091322, -3946900, 23104804, -12869908, 5727338, 189038 }, + { 14609123, -8954470, -6000566, -16622781, -14577387, -7743898, -26745169, 10942115, -25888931, -14884697 }, + }, + { + { 20513500, 5557931, -15604613, 7829531, 26413943, -2019404, -21378968, 7471781, 13913677, -5137875 }, + { -25574376, 11967826, 29233242, 12948236, -6754465, 4713227, -8940970, 14059180, 12878652, 8511905 }, + { -25656801, 3393631, -2955415, -7075526, -2250709, 9366908, -30223418, 6812974, 5568676, -3127656 }, + }, + { + { 11630004, 12144454, 2116339, 13606037, 27378885, 15676917, -17408753, -13504373, -14395196, 8070818 }, + { 27117696, -10007378, -31282771, -5570088, 1127282, 12772488, -29845906, 10483306, -11552749, -1028714 }, + { 10637467, -5688064, 5674781, 1072708, -26343588, -6982302, -1683975, 9177853, -27493162, 15431203 }, + }, + { + { 20525145, 10892566, -12742472, 12779443, -29493034, 16150075, -28240519, 14943142, -15056790, -7935931 }, + { -30024462, 5626926, -551567, -9981087, 753598, 11981191, 25244767, -3239766, -3356550, 9594024 }, + { -23752644, 2636870, -5163910, -10103818, 585134, 7877383, 11345683, -6492290, 13352335, -10977084 }, + }, + { + { -1931799, -5407458, 3304649, -12884869, 17015806, -4877091, -29783850, -7752482, -13215537, -319204 }, + { 20239939, 6607058, 6203985, 3483793, -18386976, -779229, -20723742, 15077870, -22750759, 14523817 }, + { 27406042, -6041657, 27423596, -4497394, 4996214, 10002360, -28842031, -4545494, -30172742, -4805667 }, + }, + }, + { + { + { 11374242, 12660715, 17861383, -12540833, 10935568, 1099227, -13886076, -9091740, -27727044, 11358504 }, + { -12730809, 10311867, 1510375, 10778093, -2119455, -9145702, 32676003, 11149336, -26123651, 4985768 }, + { -19096303, 341147, -6197485, -239033, 15756973, -8796662, -983043, 13794114, -19414307, -15621255 }, + }, + { + { 6490081, 11940286, 25495923, -7726360, 8668373, -8751316, 3367603, 6970005, -1691065, -9004790 }, + { 1656497, 13457317, 15370807, 6364910, 13605745, 8362338, -19174622, -5475723, -16796596, -5031438 }, + { -22273315, -13524424, -64685, -4334223, -18605636, -10921968, -20571065, -7007978, -99853, -10237333 }, + }, + { + { 17747465, 10039260, 19368299, -4050591, -20630635, -16041286, 31992683, -15857976, -29260363, -5511971 }, + { 31932027, -4986141, -19612382, 16366580, 22023614, 88450, 11371999, -3744247, 4882242, -10626905 }, + { 29796507, 37186, 19818052, 10115756, -11829032, 3352736, 18551198, 3272828, -5190932, -4162409 }, + }, + { + { 12501286, 4044383, -8612957, -13392385, -32430052, 5136599, -19230378, -3529697, 330070, -3659409 }, + { 6384877, 2899513, 17807477, 7663917, -2358888, 12363165, 25366522, -8573892, -271295, 12071499 }, + { -8365515, -4042521, 25133448, -4517355, -6211027, 2265927, -32769618, 1936675, -5159697, 3829363 }, + }, + { + { 28425966, -5835433, -577090, -4697198, -14217555, 6870930, 7921550, -6567787, 26333140, 14267664 }, + { -11067219, 11871231, 27385719, -10559544, -4585914, -11189312, 10004786, -8709488, -21761224, 8930324 }, + { -21197785, -16396035, 25654216, -1725397, 12282012, 11008919, 1541940, 4757911, -26491501, -16408940 }, + }, + { + { 13537262, -7759490, -20604840, 10961927, -5922820, -13218065, -13156584, 6217254, -15943699, 13814990 }, + { -17422573, 15157790, 18705543, 29619, 24409717, -260476, 27361681, 9257833, -1956526, -1776914 }, + { -25045300, -10191966, 15366585, 15166509, -13105086, 8423556, -29171540, 12361135, -18685978, 4578290 }, + }, + { + { 24579768, 3711570, 1342322, -11180126, -27005135, 14124956, -22544529, 14074919, 21964432, 8235257 }, + { -6528613, -2411497, 9442966, -5925588, 12025640, -1487420, -2981514, -1669206, 13006806, 2355433 }, + { -16304899, -13605259, -6632427, -5142349, 16974359, -10911083, 27202044, 1719366, 1141648, -12796236 }, + }, + { + { -12863944, -13219986, -8318266, -11018091, -6810145, -4843894, 13475066, -3133972, 32674895, 13715045 }, + { 11423335, -5468059, 32344216, 8962751, 24989809, 9241752, -13265253, 16086212, -28740881, -15642093 }, + { -1409668, 12530728, -6368726, 10847387, 19531186, -14132160, -11709148, 7791794, -27245943, 4383347 }, + }, + }, + { + { + { -28970898, 5271447, -1266009, -9736989, -12455236, 16732599, -4862407, -4906449, 27193557, 6245191 }, + { -15193956, 5362278, -1783893, 2695834, 4960227, 12840725, 23061898, 3260492, 22510453, 8577507 }, + { -12632451, 11257346, -32692994, 13548177, -721004, 10879011, 31168030, 13952092, -29571492, -3635906 }, + }, + { + { 3877321, -9572739, 32416692, 5405324, -11004407, -13656635, 3759769, 11935320, 5611860, 8164018 }, + { -16275802, 14667797, 15906460, 12155291, -22111149, -9039718, 32003002, -8832289, 5773085, -8422109 }, + { -23788118, -8254300, 1950875, 8937633, 18686727, 16459170, -905725, 12376320, 31632953, 190926 }, + }, + { + { -24593607, -16138885, -8423991, 13378746, 14162407, 6901328, -8288749, 4508564, -25341555, -3627528 }, + { 8884438, -5884009, 6023974, 10104341, -6881569, -4941533, 18722941, -14786005, -1672488, 827625 }, + { -32720583, -16289296, -32503547, 7101210, 13354605, 2659080, -1800575, -14108036, -24878478, 1541286 }, + }, + { + { 2901347, -1117687, 3880376, -10059388, -17620940, -3612781, -21802117, -3567481, 20456845, -1885033 }, + { 27019610, 12299467, -13658288, -1603234, -12861660, -4861471, -19540150, -5016058, 29439641, 15138866 }, + { 21536104, -6626420, -32447818, -10690208, -22408077, 5175814, -5420040, -16361163, 7779328, 109896 }, + }, + { + { 30279744, 14648750, -8044871, 6425558, 13639621, -743509, 28698390, 12180118, 23177719, -554075 }, + { 26572847, 3405927, -31701700, 12890905, -19265668, 5335866, -6493768, 2378492, 4439158, -13279347 }, + { -22716706, 3489070, -9225266, -332753, 18875722, -1140095, 14819434, -12731527, -17717757, -5461437 }, + }, + { + { -5056483, 16566551, 15953661, 3767752, -10436499, 15627060, -820954, 2177225, 8550082, -15114165 }, + { -18473302, 16596775, -381660, 15663611, 22860960, 15585581, -27844109, -3582739, -23260460, -8428588 }, + { -32480551, 15707275, -8205912, -5652081, 29464558, 2713815, -22725137, 15860482, -21902570, 1494193 }, + }, + { + { -19562091, -14087393, -25583872, -9299552, 13127842, 759709, 21923482, 16529112, 8742704, 12967017 }, + { -28464899, 1553205, 32536856, -10473729, -24691605, -406174, -8914625, -2933896, -29903758, 15553883 }, + { 21877909, 3230008, 9881174, 10539357, -4797115, 2841332, 11543572, 14513274, 19375923, -12647961 }, + }, + { + { 8832269, -14495485, 13253511, 5137575, 5037871, 4078777, 24880818, -6222716, 2862653, 9455043 }, + { 29306751, 5123106, 20245049, -14149889, 9592566, 8447059, -2077124, -2990080, 15511449, 4789663 }, + { -20679756, 7004547, 8824831, -9434977, -4045704, -3750736, -5754762, 108893, 23513200, 16652362 }, + }, + }, + { + { + { -33256173, 4144782, -4476029, -6579123, 10770039, -7155542, -6650416, -12936300, -18319198, 10212860 }, + { 2756081, 8598110, 7383731, -6859892, 22312759, -1105012, 21179801, 2600940, -9988298, -12506466 }, + { -24645692, 13317462, -30449259, -15653928, 21365574, -10869657, 11344424, 864440, -2499677, -16710063 }, + }, + { + { -26432803, 6148329, -17184412, -14474154, 18782929, -275997, -22561534, 211300, 2719757, 4940997 }, + { -1323882, 3911313, -6948744, 14759765, -30027150, 7851207, 21690126, 8518463, 26699843, 5276295 }, + { -13149873, -6429067, 9396249, 365013, 24703301, -10488939, 1321586, 149635, -15452774, 7159369 }, + }, + { + { 9987780, -3404759, 17507962, 9505530, 9731535, -2165514, 22356009, 8312176, 22477218, -8403385 }, + { 18155857, -16504990, 19744716, 9006923, 15154154, -10538976, 24256460, -4864995, -22548173, 9334109 }, + { 2986088, -4911893, 10776628, -3473844, 10620590, -7083203, -21413845, 14253545, -22587149, 536906 }, + }, + { + { 4377756, 8115836, 24567078, 15495314, 11625074, 13064599, 7390551, 10589625, 10838060, -15420424 }, + { -19342404, 867880, 9277171, -3218459, -14431572, -1986443, 19295826, -15796950, 6378260, 699185 }, + { 7895026, 4057113, -7081772, -13077756, -17886831, -323126, -716039, 15693155, -5045064, -13373962 }, + }, + { + { -7737563, -5869402, -14566319, -7406919, 11385654, 13201616, 31730678, -10962840, -3918636, -9669325 }, + { 10188286, -15770834, -7336361, 13427543, 22223443, 14896287, 30743455, 7116568, -21786507, 5427593 }, + { 696102, 13206899, 27047647, -10632082, 15285305, -9853179, 10798490, -4578720, 19236243, 12477404 }, + }, + { + { -11229439, 11243796, -17054270, -8040865, -788228, -8167967, -3897669, 11180504, -23169516, 7733644 }, + { 17800790, -14036179, -27000429, -11766671, 23887827, 3149671, 23466177, -10538171, 10322027, 15313801 }, + { 26246234, 11968874, 32263343, -5468728, 6830755, -13323031, -15794704, -101982, -24449242, 10890804 }, + }, + { + { -31365647, 10271363, -12660625, -6267268, 16690207, -13062544, -14982212, 16484931, 25180797, -5334884 }, + { -586574, 10376444, -32586414, -11286356, 19801893, 10997610, 2276632, 9482883, 316878, 13820577 }, + { -9882808, -4510367, -2115506, 16457136, -11100081, 11674996, 30756178, -7515054, 30696930, -3712849 }, + }, + { + { 32988917, -9603412, 12499366, 7910787, -10617257, -11931514, -7342816, -9985397, -32349517, 7392473 }, + { -8855661, 15927861, 9866406, -3649411, -2396914, -16655781, -30409476, -9134995, 25112947, -2926644 }, + { -2504044, -436966, 25621774, -5678772, 15085042, -5479877, -24884878, -13526194, 5537438, -13914319 }, + }, + }, + { + { + { -11225584, 2320285, -9584280, 10149187, -33444663, 5808648, -14876251, -1729667, 31234590, 6090599 }, + { -9633316, 116426, 26083934, 2897444, -6364437, -2688086, 609721, 15878753, -6970405, -9034768 }, + { -27757857, 247744, -15194774, -9002551, 23288161, -10011936, -23869595, 6503646, 20650474, 1804084 }, + }, + { + { -27589786, 15456424, 8972517, 8469608, 15640622, 4439847, 3121995, -10329713, 27842616, -202328 }, + { -15306973, 2839644, 22530074, 10026331, 4602058, 5048462, 28248656, 5031932, -11375082, 12714369 }, + { 20807691, -7270825, 29286141, 11421711, -27876523, -13868230, -21227475, 1035546, -19733229, 12796920 }, + }, + { + { 12076899, -14301286, -8785001, -11848922, -25012791, 16400684, -17591495, -12899438, 3480665, -15182815 }, + { -32361549, 5457597, 28548107, 7833186, 7303070, -11953545, -24363064, -15921875, -33374054, 2771025 }, + { -21389266, 421932, 26597266, 6860826, 22486084, -6737172, -17137485, -4210226, -24552282, 15673397 }, + }, + { + { -20184622, 2338216, 19788685, -9620956, -4001265, -8740893, -20271184, 4733254, 3727144, -12934448 }, + { 6120119, 814863, -11794402, -622716, 6812205, -15747771, 2019594, 7975683, 31123697, -10958981 }, + { 30069250, -11435332, 30434654, 2958439, 18399564, -976289, 12296869, 9204260, -16432438, 9648165 }, + }, + { + { 32705432, -1550977, 30705658, 7451065, -11805606, 9631813, 3305266, 5248604, -26008332, -11377501 }, + { 17219865, 2375039, -31570947, -5575615, -19459679, 9219903, 294711, 15298639, 2662509, -16297073 }, + { -1172927, -7558695, -4366770, -4287744, -21346413, -8434326, 32087529, -1222777, 32247248, -14389861 }, + }, + { + { 14312628, 1221556, 17395390, -8700143, -4945741, -8684635, -28197744, -9637817, -16027623, -13378845 }, + { -1428825, -9678990, -9235681, 6549687, -7383069, -468664, 23046502, 9803137, 17597934, 2346211 }, + { 18510800, 15337574, 26171504, 981392, -22241552, 7827556, -23491134, -11323352, 3059833, -11782870 }, + }, + { + { 10141598, 6082907, 17829293, -1947643, 9830092, 13613136, -25556636, -5544586, -33502212, 3592096 }, + { 33114168, -15889352, -26525686, -13343397, 33076705, 8716171, 1151462, 1521897, -982665, -6837803 }, + { -32939165, -4255815, 23947181, -324178, -33072974, -12305637, -16637686, 3891704, 26353178, 693168 }, + }, + { + { 30374239, 1595580, -16884039, 13186931, 4600344, 406904, 9585294, -400668, 31375464, 14369965 }, + { -14370654, -7772529, 1510301, 6434173, -18784789, -6262728, 32732230, -13108839, 17901441, 16011505 }, + { 18171223, -11934626, -12500402, 15197122, -11038147, -15230035, -19172240, -16046376, 8764035, 12309598 }, + }, + }, + { + { + { 5975908, -5243188, -19459362, -9681747, -11541277, 14015782, -23665757, 1228319, 17544096, -10593782 }, + { 5811932, -1715293, 3442887, -2269310, -18367348, -8359541, -18044043, -15410127, -5565381, 12348900 }, + { -31399660, 11407555, 25755363, 6891399, -3256938, 14872274, -24849353, 8141295, -10632534, -585479 }, + }, + { + { -12675304, 694026, -5076145, 13300344, 14015258, -14451394, -9698672, -11329050, 30944593, 1130208 }, + { 8247766, -6710942, -26562381, -7709309, -14401939, -14648910, 4652152, 2488540, 23550156, -271232 }, + { 17294316, -3788438, 7026748, 15626851, 22990044, 113481, 2267737, -5908146, -408818, -137719 }, + }, + { + { 16091085, -16253926, 18599252, 7340678, 2137637, -1221657, -3364161, 14550936, 3260525, -7166271 }, + { -4910104, -13332887, 18550887, 10864893, -16459325, -7291596, -23028869, -13204905, -12748722, 2701326 }, + { -8574695, 16099415, 4629974, -16340524, -20786213, -6005432, -10018363, 9276971, 11329923, 1862132 }, + }, + { + { 14763076, -15903608, -30918270, 3689867, 3511892, 10313526, -21951088, 12219231, -9037963, -940300 }, + { 8894987, -3446094, 6150753, 3013931, 301220, 15693451, -31981216, -2909717, -15438168, 11595570 }, + { 15214962, 3537601, -26238722, -14058872, 4418657, -15230761, 13947276, 10730794, -13489462, -4363670 }, + }, + { + { -2538306, 7682793, 32759013, 263109, -29984731, -7955452, -22332124, -10188635, 977108, 699994 }, + { -12466472, 4195084, -9211532, 550904, -15565337, 12917920, 19118110, -439841, -30534533, -14337913 }, + { 31788461, -14507657, 4799989, 7372237, 8808585, -14747943, 9408237, -10051775, 12493932, -5409317 }, + }, + { + { -25680606, 5260744, -19235809, -6284470, -3695942, 16566087, 27218280, 2607121, 29375955, 6024730 }, + { 842132, -2794693, -4763381, -8722815, 26332018, -12405641, 11831880, 6985184, -9940361, 2854096 }, + { -4847262, -7969331, 2516242, -5847713, 9695691, -7221186, 16512645, 960770, 12121869, 16648078 }, + }, + { + { -15218652, 14667096, -13336229, 2013717, 30598287, -464137, -31504922, -7882064, 20237806, 2838411 }, + { -19288047, 4453152, 15298546, -16178388, 22115043, -15972604, 12544294, -13470457, 1068881, -12499905 }, + { -9558883, -16518835, 33238498, 13506958, 30505848, -1114596, -8486907, -2630053, 12521378, 4845654 }, + }, + { + { -28198521, 10744108, -2958380, 10199664, 7759311, -13088600, 3409348, -873400, -6482306, -12885870 }, + { -23561822, 6230156, -20382013, 10655314, -24040585, -11621172, 10477734, -1240216, -3113227, 13974498 }, + { 12966261, 15550616, -32038948, -1615346, 21025980, -629444, 5642325, 7188737, 18895762, 12629579 }, + }, + }, + { + { + { 14741879, -14946887, 22177208, -11721237, 1279741, 8058600, 11758140, 789443, 32195181, 3895677 }, + { 10758205, 15755439, -4509950, 9243698, -4879422, 6879879, -2204575, -3566119, -8982069, 4429647 }, + { -2453894, 15725973, -20436342, -10410672, -5803908, -11040220, -7135870, -11642895, 18047436, -15281743 }, + }, + { + { -25173001, -11307165, 29759956, 11776784, -22262383, -15820455, 10993114, -12850837, -17620701, -9408468 }, + { 21987233, 700364, -24505048, 14972008, -7774265, -5718395, 32155026, 2581431, -29958985, 8773375 }, + { -25568350, 454463, -13211935, 16126715, 25240068, 8594567, 20656846, 12017935, -7874389, -13920155 }, + }, + { + { 6028182, 6263078, -31011806, -11301710, -818919, 2461772, -31841174, -5468042, -1721788, -2776725 }, + { -12278994, 16624277, 987579, -5922598, 32908203, 1248608, 7719845, -4166698, 28408820, 6816612 }, + { -10358094, -8237829, 19549651, -12169222, 22082623, 16147817, 20613181, 13982702, -10339570, 5067943 }, + }, + { + { -30505967, -3821767, 12074681, 13582412, -19877972, 2443951, -19719286, 12746132, 5331210, -10105944 }, + { 30528811, 3601899, -1957090, 4619785, -27361822, -15436388, 24180793, -12570394, 27679908, -1648928 }, + { 9402404, -13957065, 32834043, 10838634, -26580150, -13237195, 26653274, -8685565, 22611444, -12715406 }, + }, + { + { 22190590, 1118029, 22736441, 15130463, -30460692, -5991321, 19189625, -4648942, 4854859, 6622139 }, + { -8310738, -2953450, -8262579, -3388049, -10401731, -271929, 13424426, -3567227, 26404409, 13001963 }, + { -31241838, -15415700, -2994250, 8939346, 11562230, -12840670, -26064365, -11621720, -15405155, 11020693 }, + }, + { + { 1866042, -7949489, -7898649, -10301010, 12483315, 13477547, 3175636, -12424163, 28761762, 1406734 }, + { -448555, -1777666, 13018551, 3194501, -9580420, -11161737, 24760585, -4347088, 25577411, -13378680 }, + { -24290378, 4759345, -690653, -1852816, 2066747, 10693769, -29595790, 9884936, -9368926, 4745410 }, + }, + { + { -9141284, 6049714, -19531061, -4341411, -31260798, 9944276, -15462008, -11311852, 10931924, -11931931 }, + { -16561513, 14112680, -8012645, 4817318, -8040464, -11414606, -22853429, 10856641, -20470770, 13434654 }, + { 22759489, -10073434, -16766264, -1871422, 13637442, -10168091, 1765144, -12654326, 28445307, -5364710 }, + }, + { + { 29875063, 12493613, 2795536, -3786330, 1710620, 15181182, -10195717, -8788675, 9074234, 1167180 }, + { -26205683, 11014233, -9842651, -2635485, -26908120, 7532294, -18716888, -9535498, 3843903, 9367684 }, + { -10969595, -6403711, 9591134, 9582310, 11349256, 108879, 16235123, 8601684, -139197, 4242895 }, + }, + }, + { + { + { 22092954, -13191123, -2042793, -11968512, 32186753, -11517388, -6574341, 2470660, -27417366, 16625501 }, + { -11057722, 3042016, 13770083, -9257922, 584236, -544855, -7770857, 2602725, -27351616, 14247413 }, + { 6314175, -10264892, -32772502, 15957557, -10157730, 168750, -8618807, 14290061, 27108877, -1180880 }, + }, + { + { -8586597, -7170966, 13241782, 10960156, -32991015, -13794596, 33547976, -11058889, -27148451, 981874 }, + { 22833440, 9293594, -32649448, -13618667, -9136966, 14756819, -22928859, -13970780, -10479804, -16197962 }, + { -7768587, 3326786, -28111797, 10783824, 19178761, 14905060, 22680049, 13906969, -15933690, 3797899 }, + }, + { + { 21721356, -4212746, -12206123, 9310182, -3882239, -13653110, 23740224, -2709232, 20491983, -8042152 }, + { 9209270, -15135055, -13256557, -6167798, -731016, 15289673, 25947805, 15286587, 30997318, -6703063 }, + { 7392032, 16618386, 23946583, -8039892, -13265164, -1533858, -14197445, -2321576, 17649998, -250080 }, + }, + { + { -9301088, -14193827, 30609526, -3049543, -25175069, -1283752, -15241566, -9525724, -2233253, 7662146 }, + { -17558673, 1763594, -33114336, 15908610, -30040870, -12174295, 7335080, -8472199, -3174674, 3440183 }, + { -19889700, -5977008, -24111293, -9688870, 10799743, -16571957, 40450, -4431835, 4862400, 1133 }, + }, + { + { -32856209, -7873957, -5422389, 14860950, -16319031, 7956142, 7258061, 311861, -30594991, -7379421 }, + { -3773428, -1565936, 28985340, 7499440, 24445838, 9325937, 29727763, 16527196, 18278453, 15405622 }, + { -4381906, 8508652, -19898366, -3674424, -5984453, 15149970, -13313598, 843523, -21875062, 13626197 }, + }, + { + { 2281448, -13487055, -10915418, -2609910, 1879358, 16164207, -10783882, 3953792, 13340839, 15928663 }, + { 31727126, -7179855, -18437503, -8283652, 2875793, -16390330, -25269894, -7014826, -23452306, 5964753 }, + { 4100420, -5959452, -17179337, 6017714, -18705837, 12227141, -26684835, 11344144, 2538215, -7570755 }, + }, + { + { -9433605, 6123113, 11159803, -2156608, 30016280, 14966241, -20474983, 1485421, -629256, -15958862 }, + { -26804558, 4260919, 11851389, 9658551, -32017107, 16367492, -20205425, -13191288, 11659922, -11115118 }, + { 26180396, 10015009, -30844224, -8581293, 5418197, 9480663, 2231568, -10170080, 33100372, -1306171 }, + }, + { + { 15121113, -5201871, -10389905, 15427821, -27509937, -15992507, 21670947, 4486675, -5931810, -14466380 }, + { 16166486, -9483733, -11104130, 6023908, -31926798, -1364923, 2340060, -16254968, -10735770, -10039824 }, + { 28042865, -3557089, -12126526, 12259706, -3717498, -6945899, 6766453, -8689599, 18036436, 5803270 }, + }, + }, + { + { + { -817581, 6763912, 11803561, 1585585, 10958447, -2671165, 23855391, 4598332, -6159431, -14117438 }, + { -31031306, -14256194, 17332029, -2383520, 31312682, -5967183, 696309, 50292, -20095739, 11763584 }, + { -594563, -2514283, -32234153, 12643980, 12650761, 14811489, 665117, -12613632, -19773211, -10713562 }, + }, + { + { 30464590, -11262872, -4127476, -12734478, 19835327, -7105613, -24396175, 2075773, -17020157, 992471 }, + { 18357185, -6994433, 7766382, 16342475, -29324918, 411174, 14578841, 8080033, -11574335, -10601610 }, + { 19598397, 10334610, 12555054, 2555664, 18821899, -10339780, 21873263, 16014234, 26224780, 16452269 }, + }, + { + { -30223925, 5145196, 5944548, 16385966, 3976735, 2009897, -11377804, -7618186, -20533829, 3698650 }, + { 14187449, 3448569, -10636236, -10810935, -22663880, -3433596, 7268410, -10890444, 27394301, 12015369 }, + { 19695761, 16087646, 28032085, 12999827, 6817792, 11427614, 20244189, -1312777, -13259127, -3402461 }, + }, + { + { 30860103, 12735208, -1888245, -4699734, -16974906, 2256940, -8166013, 12298312, -8550524, -10393462 }, + { -5719826, -11245325, -1910649, 15569035, 26642876, -7587760, -5789354, -15118654, -4976164, 12651793 }, + { -2848395, 9953421, 11531313, -5282879, 26895123, -12697089, -13118820, -16517902, 9768698, -2533218 }, + }, + { + { -24719459, 1894651, -287698, -4704085, 15348719, -8156530, 32767513, 12765450, 4940095, 10678226 }, + { 18860224, 15980149, -18987240, -1562570, -26233012, -11071856, -7843882, 13944024, -24372348, 16582019 }, + { -15504260, 4970268, -29893044, 4175593, -20993212, -2199756, -11704054, 15444560, -11003761, 7989037 }, + }, + { + { 31490452, 5568061, -2412803, 2182383, -32336847, 4531686, -32078269, 6200206, -19686113, -14800171 }, + { -17308668, -15879940, -31522777, -2831, -32887382, 16375549, 8680158, -16371713, 28550068, -6857132 }, + { -28126887, -5688091, 16837845, -1820458, -6850681, 12700016, -30039981, 4364038, 1155602, 5988841 }, + }, + { + { 21890435, -13272907, -12624011, 12154349, -7831873, 15300496, 23148983, -4470481, 24618407, 8283181 }, + { -33136107, -10512751, 9975416, 6841041, -31559793, 16356536, 3070187, -7025928, 1466169, 10740210 }, + { -1509399, -15488185, -13503385, -10655916, 32799044, 909394, -13938903, -5779719, -32164649, -15327040 }, + }, + { + { 3960823, -14267803, -28026090, -15918051, -19404858, 13146868, 15567327, 951507, -3260321, -573935 }, + { 24740841, 5052253, -30094131, 8961361, 25877428, 6165135, -24368180, 14397372, -7380369, -6144105 }, + { -28888365, 3510803, -28103278, -1158478, -11238128, -10631454, -15441463, -14453128, -1625486, -6494814 }, + }, + }, + { + { + { 793299, -9230478, 8836302, -6235707, -27360908, -2369593, 33152843, -4885251, -9906200, -621852 }, + { 5666233, 525582, 20782575, -8038419, -24538499, 14657740, 16099374, 1468826, -6171428, -15186581 }, + { -4859255, -3779343, -2917758, -6748019, 7778750, 11688288, -30404353, -9871238, -1558923, -9863646 }, + }, + { + { 10896332, -7719704, 824275, 472601, -19460308, 3009587, 25248958, 14783338, -30581476, -15757844 }, + { 10566929, 12612572, -31944212, 11118703, -12633376, 12362879, 21752402, 8822496, 24003793, 14264025 }, + { 27713862, -7355973, -11008240, 9227530, 27050101, 2504721, 23886875, -13117525, 13958495, -5732453 }, + }, + { + { -23481610, 4867226, -27247128, 3900521, 29838369, -8212291, -31889399, -10041781, 7340521, -15410068 }, + { 4646514, -8011124, -22766023, -11532654, 23184553, 8566613, 31366726, -1381061, -15066784, -10375192 }, + { -17270517, 12723032, -16993061, 14878794, 21619651, -6197576, 27584817, 3093888, -8843694, 3849921 }, + }, + { + { -9064912, 2103172, 25561640, -15125738, -5239824, 9582958, 32477045, -9017955, 5002294, -15550259 }, + { -12057553, -11177906, 21115585, -13365155, 8808712, -12030708, 16489530, 13378448, -25845716, 12741426 }, + { -5946367, 10645103, -30911586, 15390284, -3286982, -7118677, 24306472, 15852464, 28834118, -7646072 }, + }, + { + { -17335748, -9107057, -24531279, 9434953, -8472084, -583362, -13090771, 455841, 20461858, 5491305 }, + { 13669248, -16095482, -12481974, -10203039, -14569770, -11893198, -24995986, 11293807, -28588204, -9421832 }, + { 28497928, 6272777, -33022994, 14470570, 8906179, -1225630, 18504674, -14165166, 29867745, -8795943 }, + }, + { + { -16207023, 13517196, -27799630, -13697798, 24009064, -6373891, -6367600, -13175392, 22853429, -4012011 }, + { 24191378, 16712145, -13931797, 15217831, 14542237, 1646131, 18603514, -11037887, 12876623, -2112447 }, + { 17902668, 4518229, -411702, -2829247, 26878217, 5258055, -12860753, 608397, 16031844, 3723494 }, + }, + { + { -28632773, 12763728, -20446446, 7577504, 33001348, -13017745, 17558842, -7872890, 23896954, -4314245 }, + { -20005381, -12011952, 31520464, 605201, 2543521, 5991821, -2945064, 7229064, -9919646, -8826859 }, + { 28816045, 298879, -28165016, -15920938, 19000928, -1665890, -12680833, -2949325, -18051778, -2082915 }, + }, + { + { 16000882, -344896, 3493092, -11447198, -29504595, -13159789, 12577740, 16041268, -19715240, 7847707 }, + { 10151868, 10572098, 27312476, 7922682, 14825339, 4723128, -32855931, -6519018, -10020567, 3852848 }, + { -11430470, 15697596, -21121557, -4420647, 5386314, 15063598, 16514493, -15932110, 29330899, -15076224 }, + }, + }, + { + { + { -25499735, -4378794, -15222908, -6901211, 16615731, 2051784, 3303702, 15490, -27548796, 12314391 }, + { 15683520, -6003043, 18109120, -9980648, 15337968, -5997823, -16717435, 15921866, 16103996, -3731215 }, + { -23169824, -10781249, 13588192, -1628807, -3798557, -1074929, -19273607, 5402699, -29815713, -9841101 }, + }, + { + { 23190676, 2384583, -32714340, 3462154, -29903655, -1529132, -11266856, 8911517, -25205859, 2739713 }, + { 21374101, -3554250, -33524649, 9874411, 15377179, 11831242, -33529904, 6134907, 4931255, 11987849 }, + { -7732, -2978858, -16223486, 7277597, 105524, -322051, -31480539, 13861388, -30076310, 10117930 }, + }, + { + { -29501170, -10744872, -26163768, 13051539, -25625564, 5089643, -6325503, 6704079, 12890019, 15728940 }, + { -21972360, -11771379, -951059, -4418840, 14704840, 2695116, 903376, -10428139, 12885167, 8311031 }, + { -17516482, 5352194, 10384213, -13811658, 7506451, 13453191, 26423267, 4384730, 1888765, -5435404 }, + }, + { + { -25817338, -3107312, -13494599, -3182506, 30896459, -13921729, -32251644, -12707869, -19464434, -3340243 }, + { -23607977, -2665774, -526091, 4651136, 5765089, 4618330, 6092245, 14845197, 17151279, -9854116 }, + { -24830458, -12733720, -15165978, 10367250, -29530908, -265356, 22825805, -7087279, -16866484, 16176525 }, + }, + { + { -23583256, 6564961, 20063689, 3798228, -4740178, 7359225, 2006182, -10363426, -28746253, -10197509 }, + { -10626600, -4486402, -13320562, -5125317, 3432136, -6393229, 23632037, -1940610, 32808310, 1099883 }, + { 15030977, 5768825, -27451236, -2887299, -6427378, -15361371, -15277896, -6809350, 2051441, -15225865 }, + }, + { + { -3362323, -7239372, 7517890, 9824992, 23555850, 295369, 5148398, -14154188, -22686354, 16633660 }, + { 4577086, -16752288, 13249841, -15304328, 19958763, -14537274, 18559670, -10759549, 8402478, -9864273 }, + { -28406330, -1051581, -26790155, -907698, -17212414, -11030789, 9453451, -14980072, 17983010, 9967138 }, + }, + { + { -25762494, 6524722, 26585488, 9969270, 24709298, 1220360, -1677990, 7806337, 17507396, 3651560 }, + { -10420457, -4118111, 14584639, 15971087, -15768321, 8861010, 26556809, -5574557, -18553322, -11357135 }, + { 2839101, 14284142, 4029895, 3472686, 14402957, 12689363, -26642121, 8459447, -5605463, -7621941 }, + }, + { + { -4839289, -3535444, 9744961, 2871048, 25113978, 3187018, -25110813, -849066, 17258084, -7977739 }, + { 18164541, -10595176, -17154882, -1542417, 19237078, -9745295, 23357533, -15217008, 26908270, 12150756 }, + { -30264870, -7647865, 5112249, -7036672, -1499807, -6974257, 43168, -5537701, -32302074, 16215819 }, + }, + }, + { + { + { -6898905, 9824394, -12304779, -4401089, -31397141, -6276835, 32574489, 12532905, -7503072, -8675347 }, + { -27343522, -16515468, -27151524, -10722951, 946346, 16291093, 254968, 7168080, 21676107, -1943028 }, + { 21260961, -8424752, -16831886, -11920822, -23677961, 3968121, -3651949, -6215466, -3556191, -7913075 }, + }, + { + { 16544754, 13250366, -16804428, 15546242, -4583003, 12757258, -2462308, -8680336, -18907032, -9662799 }, + { -2415239, -15577728, 18312303, 4964443, -15272530, -12653564, 26820651, 16690659, 25459437, -4564609 }, + { -25144690, 11425020, 28423002, -11020557, -6144921, -15826224, 9142795, -2391602, -6432418, -1644817 }, + }, + { + { -23104652, 6253476, 16964147, -3768872, -25113972, -12296437, -27457225, -16344658, 6335692, 7249989 }, + { -30333227, 13979675, 7503222, -12368314, -11956721, -4621693, -30272269, 2682242, 25993170, -12478523 }, + { 4364628, 5930691, 32304656, -10044554, -8054781, 15091131, 22857016, -10598955, 31820368, 15075278 }, + }, + { + { 31879134, -8918693, 17258761, 90626, -8041836, -4917709, 24162788, -9650886, -17970238, 12833045 }, + { 19073683, 14851414, -24403169, -11860168, 7625278, 11091125, -19619190, 2074449, -9413939, 14905377 }, + { 24483667, -11935567, -2518866, -11547418, -1553130, 15355506, -25282080, 9253129, 27628530, -7555480 }, + }, + { + { 17597607, 8340603, 19355617, 552187, 26198470, -3176583, 4593324, -9157582, -14110875, 15297016 }, + { 510886, 14337390, -31785257, 16638632, 6328095, 2713355, -20217417, -11864220, 8683221, 2921426 }, + { 18606791, 11874196, 27155355, -5281482, -24031742, 6265446, -25178240, -1278924, 4674690, 13890525 }, + }, + { + { 13609624, 13069022, -27372361, -13055908, 24360586, 9592974, 14977157, 9835105, 4389687, 288396 }, + { 9922506, -519394, 13613107, 5883594, -18758345, -434263, -12304062, 8317628, 23388070, 16052080 }, + { 12720016, 11937594, -31970060, -5028689, 26900120, 8561328, -20155687, -11632979, -14754271, -10812892 }, + }, + { + { 15961858, 14150409, 26716931, -665832, -22794328, 13603569, 11829573, 7467844, -28822128, 929275 }, + { 11038231, -11582396, -27310482, -7316562, -10498527, -16307831, -23479533, -9371869, -21393143, 2465074 }, + { 20017163, -4323226, 27915242, 1529148, 12396362, 15675764, 13817261, -9658066, 2463391, -4622140 }, + }, + { + { -16358878, -12663911, -12065183, 4996454, -1256422, 1073572, 9583558, 12851107, 4003896, 12673717 }, + { -1731589, -15155870, -3262930, 16143082, 19294135, 13385325, 14741514, -9103726, 7903886, 2348101 }, + { 24536016, -16515207, 12715592, -3862155, 1511293, 10047386, -3842346, -7129159, -28377538, 10048127 }, + }, + }, + { + { + { -12622226, -6204820, 30718825, 2591312, -10617028, 12192840, 18873298, -7297090, -32297756, 15221632 }, + { -26478122, -11103864, 11546244, -1852483, 9180880, 7656409, -21343950, 2095755, 29769758, 6593415 }, + { -31994208, -2907461, 4176912, 3264766, 12538965, -868111, 26312345, -6118678, 30958054, 8292160 }, + }, + { + { 31429822, -13959116, 29173532, 15632448, 12174511, -2760094, 32808831, 3977186, 26143136, -3148876 }, + { 22648901, 1402143, -22799984, 13746059, 7936347, 365344, -8668633, -1674433, -3758243, -2304625 }, + { -15491917, 8012313, -2514730, -12702462, -23965846, -10254029, -1612713, -1535569, -16664475, 8194478 }, + }, + { + { 27338066, -7507420, -7414224, 10140405, -19026427, -6589889, 27277191, 8855376, 28572286, 3005164 }, + { 26287124, 4821776, 25476601, -4145903, -3764513, -15788984, -18008582, 1182479, -26094821, -13079595 }, + { -7171154, 3178080, 23970071, 6201893, -17195577, -4489192, -21876275, -13982627, 32208683, -1198248 }, + }, + { + { -16657702, 2817643, -10286362, 14811298, 6024667, 13349505, -27315504, -10497842, -27672585, -11539858 }, + { 15941029, -9405932, -21367050, 8062055, 31876073, -238629, -15278393, -1444429, 15397331, -4130193 }, + { 8934485, -13485467, -23286397, -13423241, -32446090, 14047986, 31170398, -1441021, -27505566, 15087184 }, + }, + { + { -18357243, -2156491, 24524913, -16677868, 15520427, -6360776, -15502406, 11461896, 16788528, -5868942 }, + { -1947386, 16013773, 21750665, 3714552, -17401782, -16055433, -3770287, -10323320, 31322514, -11615635 }, + { 21426655, -5650218, -13648287, -5347537, -28812189, -4920970, -18275391, -14621414, 13040862, -12112948 }, + }, + { + { 11293895, 12478086, -27136401, 15083750, -29307421, 14748872, 14555558, -13417103, 1613711, 4896935 }, + { -25894883, 15323294, -8489791, -8057900, 25967126, -13425460, 2825960, -4897045, -23971776, -11267415 }, + { -15924766, -5229880, -17443532, 6410664, 3622847, 10243618, 20615400, 12405433, -23753030, -8436416 }, + }, + { + { -7091295, 12556208, -20191352, 9025187, -17072479, 4333801, 4378436, 2432030, 23097949, -566018 }, + { 4565804, -16025654, 20084412, -7842817, 1724999, 189254, 24767264, 10103221, -18512313, 2424778 }, + { 366633, -11976806, 8173090, -6890119, 30788634, 5745705, -7168678, 1344109, -3642553, 12412659 }, + }, + { + { -24001791, 7690286, 14929416, -168257, -32210835, -13412986, 24162697, -15326504, -3141501, 11179385 }, + { 18289522, -14724954, 8056945, 16430056, -21729724, 7842514, -6001441, -1486897, -18684645, -11443503 }, + { 476239, 6601091, -6152790, -9723375, 17503545, -4863900, 27672959, 13403813, 11052904, 5219329 }, + }, + }, + { + { + { 20678546, -8375738, -32671898, 8849123, -5009758, 14574752, 31186971, -3973730, 9014762, -8579056 }, + { -13644050, -10350239, -15962508, 5075808, -1514661, -11534600, -33102500, 9160280, 8473550, -3256838 }, + { 24900749, 14435722, 17209120, -15292541, -22592275, 9878983, -7689309, -16335821, -24568481, 11788948 }, + }, + { + { -3118155, -11395194, -13802089, 14797441, 9652448, -6845904, -20037437, 10410733, -24568470, -1458691 }, + { -15659161, 16736706, -22467150, 10215878, -9097177, 7563911, 11871841, -12505194, -18513325, 8464118 }, + { -23400612, 8348507, -14585951, -861714, -3950205, -6373419, 14325289, 8628612, 33313881, -8370517 }, + }, + { + { -20186973, -4967935, 22367356, 5271547, -1097117, -4788838, -24805667, -10236854, -8940735, -5818269 }, + { -6948785, -1795212, -32625683, -16021179, 32635414, -7374245, 15989197, -12838188, 28358192, -4253904 }, + { -23561781, -2799059, -32351682, -1661963, -9147719, 10429267, -16637684, 4072016, -5351664, 5596589 }, + }, + { + { -28236598, -3390048, 12312896, 6213178, 3117142, 16078565, 29266239, 2557221, 1768301, 15373193 }, + { -7243358, -3246960, -4593467, -7553353, -127927, -912245, -1090902, -4504991, -24660491, 3442910 }, + { -30210571, 5124043, 14181784, 8197961, 18964734, -11939093, 22597931, 7176455, -18585478, 13365930 }, + }, + { + { -7877390, -1499958, 8324673, 4690079, 6261860, 890446, 24538107, -8570186, -9689599, -3031667 }, + { 25008904, -10771599, -4305031, -9638010, 16265036, 15721635, 683793, -11823784, 15723479, -15163481 }, + { -9660625, 12374379, -27006999, -7026148, -7724114, -12314514, 11879682, 5400171, 519526, -1235876 }, + }, + { + { 22258397, -16332233, -7869817, 14613016, -22520255, -2950923, -20353881, 7315967, 16648397, 7605640 }, + { -8081308, -8464597, -8223311, 9719710, 19259459, -15348212, 23994942, -5281555, -9468848, 4763278 }, + { -21699244, 9220969, -15730624, 1084137, -25476107, -2852390, 31088447, -7764523, -11356529, 728112 }, + }, + { + { 26047220, -11751471, -6900323, -16521798, 24092068, 9158119, -4273545, -12555558, -29365436, -5498272 }, + { 17510331, -322857, 5854289, 8403524, 17133918, -3112612, -28111007, 12327945, 10750447, 10014012 }, + { -10312768, 3936952, 9156313, -8897683, 16498692, -994647, -27481051, -666732, 3424691, 7540221 }, + }, + { + { 30322361, -6964110, 11361005, -4143317, 7433304, 4989748, -7071422, -16317219, -9244265, 15258046 }, + { 13054562, -2779497, 19155474, 469045, -12482797, 4566042, 5631406, 2711395, 1062915, -5136345 }, + { -19240248, -11254599, -29509029, -7499965, -5835763, 13005411, -6066489, 12194497, 32960380, 1459310 }, + }, + }, + { + { + { 19852034, 7027924, 23669353, 10020366, 8586503, -6657907, 394197, -6101885, 18638003, -11174937 }, + { 31395534, 15098109, 26581030, 8030562, -16527914, -5007134, 9012486, -7584354, -6643087, -5442636 }, + { -9192165, -2347377, -1997099, 4529534, 25766844, 607986, -13222, 9677543, -32294889, -6456008 }, + }, + { + { -2444496, -149937, 29348902, 8186665, 1873760, 12489863, -30934579, -7839692, -7852844, -8138429 }, + { -15236356, -15433509, 7766470, 746860, 26346930, -10221762, -27333451, 10754588, -9431476, 5203576 }, + { 31834314, 14135496, -770007, 5159118, 20917671, -16768096, -7467973, -7337524, 31809243, 7347066 }, + }, + { + { -9606723, -11874240, 20414459, 13033986, 13716524, -11691881, 19797970, -12211255, 15192876, -2087490 }, + { -12663563, -2181719, 1168162, -3804809, 26747877, -14138091, 10609330, 12694420, 33473243, -13382104 }, + { 33184999, 11180355, 15832085, -11385430, -1633671, 225884, 15089336, -11023903, -6135662, 14480053 }, + }, + { + { 31308717, -5619998, 31030840, -1897099, 15674547, -6582883, 5496208, 13685227, 27595050, 8737275 }, + { -20318852, -15150239, 10933843, -16178022, 8335352, -7546022, -31008351, -12610604, 26498114, 66511 }, + { 22644454, -8761729, -16671776, 4884562, -3105614, -13559366, 30540766, -4286747, -13327787, -7515095 }, + }, + { + { -28017847, 9834845, 18617207, -2681312, -3401956, -13307506, 8205540, 13585437, -17127465, 15115439 }, + { 23711543, -672915, 31206561, -8362711, 6164647, -9709987, -33535882, -1426096, 8236921, 16492939 }, + { -23910559, -13515526, -26299483, -4503841, 25005590, -7687270, 19574902, 10071562, 6708380, -6222424 }, + }, + { + { 2101391, -4930054, 19702731, 2367575, -15427167, 1047675, 5301017, 9328700, 29955601, -11678310 }, + { 3096359, 9271816, -21620864, -15521844, -14847996, -7592937, -25892142, -12635595, -9917575, 6216608 }, + { -32615849, 338663, -25195611, 2510422, -29213566, -13820213, 24822830, -6146567, -26767480, 7525079 }, + }, + { + { -23066649, -13985623, 16133487, -7896178, -3389565, 778788, -910336, -2782495, -19386633, 11994101 }, + { 21691500, -13624626, -641331, -14367021, 3285881, -3483596, -25064666, 9718258, -7477437, 13381418 }, + { 18445390, -4202236, 14979846, 11622458, -1727110, -3582980, 23111648, -6375247, 28535282, 15779576 }, + }, + { + { 30098053, 3089662, -9234387, 16662135, -21306940, 11308411, -14068454, 12021730, 9955285, -16303356 }, + { 9734894, -14576830, -7473633, -9138735, 2060392, 11313496, -18426029, 9924399, 20194861, 13380996 }, + { -26378102, -7965207, -22167821, 15789297, -18055342, -6168792, -1984914, 15707771, 26342023, 10146099 }, + }, + }, + { + { + { -26016874, -219943, 21339191, -41388, 19745256, -2878700, -29637280, 2227040, 21612326, -545728 }, + { -13077387, 1184228, 23562814, -5970442, -20351244, -6348714, 25764461, 12243797, -20856566, 11649658 }, + { -10031494, 11262626, 27384172, 2271902, 26947504, -15997771, 39944, 6114064, 33514190, 2333242 }, + }, + { + { -21433588, -12421821, 8119782, 7219913, -21830522, -9016134, -6679750, -12670638, 24350578, -13450001 }, + { -4116307, -11271533, -23886186, 4843615, -30088339, 690623, -31536088, -10406836, 8317860, 12352766 }, + { 18200138, -14475911, -33087759, -2696619, -23702521, -9102511, -23552096, -2287550, 20712163, 6719373 }, + }, + { + { 26656208, 6075253, -7858556, 1886072, -28344043, 4262326, 11117530, -3763210, 26224235, -3297458 }, + { -17168938, -14854097, -3395676, -16369877, -19954045, 14050420, 21728352, 9493610, 18620611, -16428628 }, + { -13323321, 13325349, 11432106, 5964811, 18609221, 6062965, -5269471, -9725556, -30701573, -16479657 }, + }, + { + { -23860538, -11233159, 26961357, 1640861, -32413112, -16737940, 12248509, -5240639, 13735342, 1934062 }, + { 25089769, 6742589, 17081145, -13406266, 21909293, -16067981, -15136294, -3765346, -21277997, 5473616 }, + { 31883677, -7961101, 1083432, -11572403, 22828471, 13290673, -7125085, 12469656, 29111212, -5451014 }, + }, + { + { 24244947, -15050407, -26262976, 2791540, -14997599, 16666678, 24367466, 6388839, -10295587, 452383 }, + { -25640782, -3417841, 5217916, 16224624, 19987036, -4082269, -24236251, -5915248, 15766062, 8407814 }, + { -20406999, 13990231, 15495425, 16395525, 5377168, 15166495, -8917023, -4388953, -8067909, 2276718 }, + }, + { + { 30157918, 12924066, -17712050, 9245753, 19895028, 3368142, -23827587, 5096219, 22740376, -7303417 }, + { 2041139, -14256350, 7783687, 13876377, -25946985, -13352459, 24051124, 13742383, -15637599, 13295222 }, + { 33338237, -8505733, 12532113, 7977527, 9106186, -1715251, -17720195, -4612972, -4451357, -14669444 }, + }, + { + { -20045281, 5454097, -14346548, 6447146, 28862071, 1883651, -2469266, -4141880, 7770569, 9620597 }, + { 23208068, 7979712, 33071466, 8149229, 1758231, -10834995, 30945528, -1694323, -33502340, -14767970 }, + { 1439958, -16270480, -1079989, -793782, 4625402, 10647766, -5043801, 1220118, 30494170, -11440799 }, + }, + { + { -5037580, -13028295, -2970559, -3061767, 15640974, -6701666, -26739026, 926050, -1684339, -13333647 }, + { 13908495, -3549272, 30919928, -6273825, -21521863, 7989039, 9021034, 9078865, 3353509, 4033511 }, + { -29663431, -15113610, 32259991, -344482, 24295849, -12912123, 23161163, 8839127, 27485041, 7356032 }, + }, + }, + { + { + { 9661027, 705443, 11980065, -5370154, -1628543, 14661173, -6346142, 2625015, 28431036, -16771834 }, + { -23839233, -8311415, -25945511, 7480958, -17681669, -8354183, -22545972, 14150565, 15970762, 4099461 }, + { 29262576, 16756590, 26350592, -8793563, 8529671, -11208050, 13617293, -9937143, 11465739, 8317062 }, + }, + { + { -25493081, -6962928, 32500200, -9419051, -23038724, -2302222, 14898637, 3848455, 20969334, -5157516 }, + { -20384450, -14347713, -18336405, 13884722, -33039454, 2842114, -21610826, -3649888, 11177095, 14989547 }, + { -24496721, -11716016, 16959896, 2278463, 12066309, 10137771, 13515641, 2581286, -28487508, 9930240 }, + }, + { + { -17751622, -2097826, 16544300, -13009300, -15914807, -14949081, 18345767, -13403753, 16291481, -5314038 }, + { -33229194, 2553288, 32678213, 9875984, 8534129, 6889387, -9676774, 6957617, 4368891, 9788741 }, + { 16660756, 7281060, -10830758, 12911820, 20108584, -8101676, -21722536, -8613148, 16250552, -11111103 }, + }, + { + { -19765507, 2390526, -16551031, 14161980, 1905286, 6414907, 4689584, 10604807, -30190403, 4782747 }, + { -1354539, 14736941, -7367442, -13292886, 7710542, -14155590, -9981571, 4383045, 22546403, 437323 }, + { 31665577, -12180464, -16186830, 1491339, -18368625, 3294682, 27343084, 2786261, -30633590, -14097016 }, + }, + { + { -14467279, -683715, -33374107, 7448552, 19294360, 14334329, -19690631, 2355319, -19284671, -6114373 }, + { 15121312, -15796162, 6377020, -6031361, -10798111, -12957845, 18952177, 15496498, -29380133, 11754228 }, + { -2637277, -13483075, 8488727, -14303896, 12728761, -1622493, 7141596, 11724556, 22761615, -10134141 }, + }, + { + { 16918416, 11729663, -18083579, 3022987, -31015732, -13339659, -28741185, -12227393, 32851222, 11717399 }, + { 11166634, 7338049, -6722523, 4531520, -29468672, -7302055, 31474879, 3483633, -1193175, -4030831 }, + { -185635, 9921305, 31456609, -13536438, -12013818, 13348923, 33142652, 6546660, -19985279, -3948376 }, + }, + { + { -32460596, 11266712, -11197107, -7899103, 31703694, 3855903, -8537131, -12833048, -30772034, -15486313 }, + { -18006477, 12709068, 3991746, -6479188, -21491523, -10550425, -31135347, -16049879, 10928917, 3011958 }, + { -6957757, -15594337, 31696059, 334240, 29576716, 14796075, -30831056, -12805180, 18008031, 10258577 }, + }, + { + { -22448644, 15655569, 7018479, -4410003, -30314266, -1201591, -1853465, 1367120, 25127874, 6671743 }, + { 29701166, -14373934, -10878120, 9279288, -17568, 13127210, 21382910, 11042292, 25838796, 4642684 }, + { -20430234, 14955537, -24126347, 8124619, -5369288, -5990470, 30468147, -13900640, 18423289, 4177476 }, + }, + }, +}; diff --git a/zephcore/lib/ed25519/sc.c b/zephcore/lib/ed25519/sc.c new file mode 100644 index 0000000..ca5bad2 --- /dev/null +++ b/zephcore/lib/ed25519/sc.c @@ -0,0 +1,809 @@ +#include "fixedint.h" +#include "sc.h" + +static uint64_t load_3(const unsigned char *in) { + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + + return result; +} + +static uint64_t load_4(const unsigned char *in) { + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + result |= ((uint64_t) in[3]) << 24; + + return result; +} + +/* +Input: + s[0]+256*s[1]+...+256^63*s[63] = s + +Output: + s[0]+256*s[1]+...+256^31*s[31] = s mod l + where l = 2^252 + 27742317777372353535851937790883648493. + Overwrites s in place. +*/ + +void sc_reduce(unsigned char *s) { + int64_t s0 = 2097151 & load_3(s); + int64_t s1 = 2097151 & (load_4(s + 2) >> 5); + int64_t s2 = 2097151 & (load_3(s + 5) >> 2); + int64_t s3 = 2097151 & (load_4(s + 7) >> 7); + int64_t s4 = 2097151 & (load_4(s + 10) >> 4); + int64_t s5 = 2097151 & (load_3(s + 13) >> 1); + int64_t s6 = 2097151 & (load_4(s + 15) >> 6); + int64_t s7 = 2097151 & (load_3(s + 18) >> 3); + int64_t s8 = 2097151 & load_3(s + 21); + int64_t s9 = 2097151 & (load_4(s + 23) >> 5); + int64_t s10 = 2097151 & (load_3(s + 26) >> 2); + int64_t s11 = 2097151 & (load_4(s + 28) >> 7); + int64_t s12 = 2097151 & (load_4(s + 31) >> 4); + int64_t s13 = 2097151 & (load_3(s + 34) >> 1); + int64_t s14 = 2097151 & (load_4(s + 36) >> 6); + int64_t s15 = 2097151 & (load_3(s + 39) >> 3); + int64_t s16 = 2097151 & load_3(s + 42); + int64_t s17 = 2097151 & (load_4(s + 44) >> 5); + int64_t s18 = 2097151 & (load_3(s + 47) >> 2); + int64_t s19 = 2097151 & (load_4(s + 49) >> 7); + int64_t s20 = 2097151 & (load_4(s + 52) >> 4); + int64_t s21 = 2097151 & (load_3(s + 55) >> 1); + int64_t s22 = 2097151 & (load_4(s + 57) >> 6); + int64_t s23 = (load_4(s + 60) >> 3); + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + int64_t carry10; + int64_t carry11; + int64_t carry12; + int64_t carry13; + int64_t carry14; + int64_t carry15; + int64_t carry16; + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + s23 = 0; + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + s22 = 0; + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + s21 = 0; + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + s20 = 0; + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + s19 = 0; + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + s18 = 0; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + s17 = 0; + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + s16 = 0; + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + s15 = 0; + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + s14 = 0; + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + s13 = 0; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 << 21; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + s[0] = (unsigned char) (s0 >> 0); + s[1] = (unsigned char) (s0 >> 8); + s[2] = (unsigned char) ((s0 >> 16) | (s1 << 5)); + s[3] = (unsigned char) (s1 >> 3); + s[4] = (unsigned char) (s1 >> 11); + s[5] = (unsigned char) ((s1 >> 19) | (s2 << 2)); + s[6] = (unsigned char) (s2 >> 6); + s[7] = (unsigned char) ((s2 >> 14) | (s3 << 7)); + s[8] = (unsigned char) (s3 >> 1); + s[9] = (unsigned char) (s3 >> 9); + s[10] = (unsigned char) ((s3 >> 17) | (s4 << 4)); + s[11] = (unsigned char) (s4 >> 4); + s[12] = (unsigned char) (s4 >> 12); + s[13] = (unsigned char) ((s4 >> 20) | (s5 << 1)); + s[14] = (unsigned char) (s5 >> 7); + s[15] = (unsigned char) ((s5 >> 15) | (s6 << 6)); + s[16] = (unsigned char) (s6 >> 2); + s[17] = (unsigned char) (s6 >> 10); + s[18] = (unsigned char) ((s6 >> 18) | (s7 << 3)); + s[19] = (unsigned char) (s7 >> 5); + s[20] = (unsigned char) (s7 >> 13); + s[21] = (unsigned char) (s8 >> 0); + s[22] = (unsigned char) (s8 >> 8); + s[23] = (unsigned char) ((s8 >> 16) | (s9 << 5)); + s[24] = (unsigned char) (s9 >> 3); + s[25] = (unsigned char) (s9 >> 11); + s[26] = (unsigned char) ((s9 >> 19) | (s10 << 2)); + s[27] = (unsigned char) (s10 >> 6); + s[28] = (unsigned char) ((s10 >> 14) | (s11 << 7)); + s[29] = (unsigned char) (s11 >> 1); + s[30] = (unsigned char) (s11 >> 9); + s[31] = (unsigned char) (s11 >> 17); +} + + + +/* +Input: + a[0]+256*a[1]+...+256^31*a[31] = a + b[0]+256*b[1]+...+256^31*b[31] = b + c[0]+256*c[1]+...+256^31*c[31] = c + +Output: + s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l + where l = 2^252 + 27742317777372353535851937790883648493. +*/ + +void sc_muladd(unsigned char *s, const unsigned char *a, const unsigned char *b, const unsigned char *c) { + int64_t a0 = 2097151 & load_3(a); + int64_t a1 = 2097151 & (load_4(a + 2) >> 5); + int64_t a2 = 2097151 & (load_3(a + 5) >> 2); + int64_t a3 = 2097151 & (load_4(a + 7) >> 7); + int64_t a4 = 2097151 & (load_4(a + 10) >> 4); + int64_t a5 = 2097151 & (load_3(a + 13) >> 1); + int64_t a6 = 2097151 & (load_4(a + 15) >> 6); + int64_t a7 = 2097151 & (load_3(a + 18) >> 3); + int64_t a8 = 2097151 & load_3(a + 21); + int64_t a9 = 2097151 & (load_4(a + 23) >> 5); + int64_t a10 = 2097151 & (load_3(a + 26) >> 2); + int64_t a11 = (load_4(a + 28) >> 7); + int64_t b0 = 2097151 & load_3(b); + int64_t b1 = 2097151 & (load_4(b + 2) >> 5); + int64_t b2 = 2097151 & (load_3(b + 5) >> 2); + int64_t b3 = 2097151 & (load_4(b + 7) >> 7); + int64_t b4 = 2097151 & (load_4(b + 10) >> 4); + int64_t b5 = 2097151 & (load_3(b + 13) >> 1); + int64_t b6 = 2097151 & (load_4(b + 15) >> 6); + int64_t b7 = 2097151 & (load_3(b + 18) >> 3); + int64_t b8 = 2097151 & load_3(b + 21); + int64_t b9 = 2097151 & (load_4(b + 23) >> 5); + int64_t b10 = 2097151 & (load_3(b + 26) >> 2); + int64_t b11 = (load_4(b + 28) >> 7); + int64_t c0 = 2097151 & load_3(c); + int64_t c1 = 2097151 & (load_4(c + 2) >> 5); + int64_t c2 = 2097151 & (load_3(c + 5) >> 2); + int64_t c3 = 2097151 & (load_4(c + 7) >> 7); + int64_t c4 = 2097151 & (load_4(c + 10) >> 4); + int64_t c5 = 2097151 & (load_3(c + 13) >> 1); + int64_t c6 = 2097151 & (load_4(c + 15) >> 6); + int64_t c7 = 2097151 & (load_3(c + 18) >> 3); + int64_t c8 = 2097151 & load_3(c + 21); + int64_t c9 = 2097151 & (load_4(c + 23) >> 5); + int64_t c10 = 2097151 & (load_3(c + 26) >> 2); + int64_t c11 = (load_4(c + 28) >> 7); + int64_t s0; + int64_t s1; + int64_t s2; + int64_t s3; + int64_t s4; + int64_t s5; + int64_t s6; + int64_t s7; + int64_t s8; + int64_t s9; + int64_t s10; + int64_t s11; + int64_t s12; + int64_t s13; + int64_t s14; + int64_t s15; + int64_t s16; + int64_t s17; + int64_t s18; + int64_t s19; + int64_t s20; + int64_t s21; + int64_t s22; + int64_t s23; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + int64_t carry10; + int64_t carry11; + int64_t carry12; + int64_t carry13; + int64_t carry14; + int64_t carry15; + int64_t carry16; + int64_t carry17; + int64_t carry18; + int64_t carry19; + int64_t carry20; + int64_t carry21; + int64_t carry22; + + s0 = c0 + a0 * b0; + s1 = c1 + a0 * b1 + a1 * b0; + s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; + s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; + s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; + s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; + s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0; + s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 + a8 * b0; + s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0; + s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0; + s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; + s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1; + s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2; + s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + a11 * b3; + s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4; + s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; + s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; + s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; + s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; + s20 = a9 * b11 + a10 * b10 + a11 * b9; + s21 = a10 * b11 + a11 * b10; + s22 = a11 * b11; + s23 = 0; + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + carry18 = (s18 + (1 << 20)) >> 21; + s19 += carry18; + s18 -= carry18 << 21; + carry20 = (s20 + (1 << 20)) >> 21; + s21 += carry20; + s20 -= carry20 << 21; + carry22 = (s22 + (1 << 20)) >> 21; + s23 += carry22; + s22 -= carry22 << 21; + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + carry17 = (s17 + (1 << 20)) >> 21; + s18 += carry17; + s17 -= carry17 << 21; + carry19 = (s19 + (1 << 20)) >> 21; + s20 += carry19; + s19 -= carry19 << 21; + carry21 = (s21 + (1 << 20)) >> 21; + s22 += carry21; + s21 -= carry21 << 21; + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + s23 = 0; + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + s22 = 0; + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + s21 = 0; + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + s20 = 0; + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + s19 = 0; + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + s18 = 0; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + s17 = 0; + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + s16 = 0; + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + s15 = 0; + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + s14 = 0; + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + s13 = 0; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 << 21; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + s[0] = (unsigned char) (s0 >> 0); + s[1] = (unsigned char) (s0 >> 8); + s[2] = (unsigned char) ((s0 >> 16) | (s1 << 5)); + s[3] = (unsigned char) (s1 >> 3); + s[4] = (unsigned char) (s1 >> 11); + s[5] = (unsigned char) ((s1 >> 19) | (s2 << 2)); + s[6] = (unsigned char) (s2 >> 6); + s[7] = (unsigned char) ((s2 >> 14) | (s3 << 7)); + s[8] = (unsigned char) (s3 >> 1); + s[9] = (unsigned char) (s3 >> 9); + s[10] = (unsigned char) ((s3 >> 17) | (s4 << 4)); + s[11] = (unsigned char) (s4 >> 4); + s[12] = (unsigned char) (s4 >> 12); + s[13] = (unsigned char) ((s4 >> 20) | (s5 << 1)); + s[14] = (unsigned char) (s5 >> 7); + s[15] = (unsigned char) ((s5 >> 15) | (s6 << 6)); + s[16] = (unsigned char) (s6 >> 2); + s[17] = (unsigned char) (s6 >> 10); + s[18] = (unsigned char) ((s6 >> 18) | (s7 << 3)); + s[19] = (unsigned char) (s7 >> 5); + s[20] = (unsigned char) (s7 >> 13); + s[21] = (unsigned char) (s8 >> 0); + s[22] = (unsigned char) (s8 >> 8); + s[23] = (unsigned char) ((s8 >> 16) | (s9 << 5)); + s[24] = (unsigned char) (s9 >> 3); + s[25] = (unsigned char) (s9 >> 11); + s[26] = (unsigned char) ((s9 >> 19) | (s10 << 2)); + s[27] = (unsigned char) (s10 >> 6); + s[28] = (unsigned char) ((s10 >> 14) | (s11 << 7)); + s[29] = (unsigned char) (s11 >> 1); + s[30] = (unsigned char) (s11 >> 9); + s[31] = (unsigned char) (s11 >> 17); +} diff --git a/zephcore/lib/ed25519/sc.h b/zephcore/lib/ed25519/sc.h new file mode 100644 index 0000000..e29e7fa --- /dev/null +++ b/zephcore/lib/ed25519/sc.h @@ -0,0 +1,12 @@ +#ifndef SC_H +#define SC_H + +/* +The set of scalars is \Z/l +where l = 2^252 + 27742317777372353535851937790883648493. +*/ + +void sc_reduce(unsigned char *s); +void sc_muladd(unsigned char *s, const unsigned char *a, const unsigned char *b, const unsigned char *c); + +#endif diff --git a/zephcore/lib/ed25519/seed.c b/zephcore/lib/ed25519/seed.c new file mode 100644 index 0000000..95ddc98 --- /dev/null +++ b/zephcore/lib/ed25519/seed.c @@ -0,0 +1,40 @@ +#include "ed_25519.h" + +#ifndef ED25519_NO_SEED + +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +int ed25519_create_seed(unsigned char *seed) { +#ifdef _WIN32 + HCRYPTPROV prov; + + if (!CryptAcquireContext(&prov, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { + return 1; + } + + if (!CryptGenRandom(prov, 32, seed)) { + CryptReleaseContext(prov, 0); + return 1; + } + + CryptReleaseContext(prov, 0); +#else + FILE *f = fopen("/dev/urandom", "rb"); + + if (f == NULL) { + return 1; + } + + fread(seed, 1, 32, f); + fclose(f); +#endif + + return 0; +} + +#endif diff --git a/zephcore/lib/ed25519/sha512.c b/zephcore/lib/ed25519/sha512.c new file mode 100644 index 0000000..cb8ae71 --- /dev/null +++ b/zephcore/lib/ed25519/sha512.c @@ -0,0 +1,275 @@ +/* LibTomCrypt, modular cryptographic library -- Tom St Denis + * + * LibTomCrypt is a library that provides various cryptographic + * algorithms in a highly modular and flexible manner. + * + * The library is free for all purposes without any express + * guarantee it works. + * + * Tom St Denis, tomstdenis@gmail.com, http://libtom.org + */ + +#include "fixedint.h" +#include "sha512.h" + +/* the K array */ +static const uint64_t K[80] = { + UINT64_C(0x428a2f98d728ae22), UINT64_C(0x7137449123ef65cd), + UINT64_C(0xb5c0fbcfec4d3b2f), UINT64_C(0xe9b5dba58189dbbc), + UINT64_C(0x3956c25bf348b538), UINT64_C(0x59f111f1b605d019), + UINT64_C(0x923f82a4af194f9b), UINT64_C(0xab1c5ed5da6d8118), + UINT64_C(0xd807aa98a3030242), UINT64_C(0x12835b0145706fbe), + UINT64_C(0x243185be4ee4b28c), UINT64_C(0x550c7dc3d5ffb4e2), + UINT64_C(0x72be5d74f27b896f), UINT64_C(0x80deb1fe3b1696b1), + UINT64_C(0x9bdc06a725c71235), UINT64_C(0xc19bf174cf692694), + UINT64_C(0xe49b69c19ef14ad2), UINT64_C(0xefbe4786384f25e3), + UINT64_C(0x0fc19dc68b8cd5b5), UINT64_C(0x240ca1cc77ac9c65), + UINT64_C(0x2de92c6f592b0275), UINT64_C(0x4a7484aa6ea6e483), + UINT64_C(0x5cb0a9dcbd41fbd4), UINT64_C(0x76f988da831153b5), + UINT64_C(0x983e5152ee66dfab), UINT64_C(0xa831c66d2db43210), + UINT64_C(0xb00327c898fb213f), UINT64_C(0xbf597fc7beef0ee4), + UINT64_C(0xc6e00bf33da88fc2), UINT64_C(0xd5a79147930aa725), + UINT64_C(0x06ca6351e003826f), UINT64_C(0x142929670a0e6e70), + UINT64_C(0x27b70a8546d22ffc), UINT64_C(0x2e1b21385c26c926), + UINT64_C(0x4d2c6dfc5ac42aed), UINT64_C(0x53380d139d95b3df), + UINT64_C(0x650a73548baf63de), UINT64_C(0x766a0abb3c77b2a8), + UINT64_C(0x81c2c92e47edaee6), UINT64_C(0x92722c851482353b), + UINT64_C(0xa2bfe8a14cf10364), UINT64_C(0xa81a664bbc423001), + UINT64_C(0xc24b8b70d0f89791), UINT64_C(0xc76c51a30654be30), + UINT64_C(0xd192e819d6ef5218), UINT64_C(0xd69906245565a910), + UINT64_C(0xf40e35855771202a), UINT64_C(0x106aa07032bbd1b8), + UINT64_C(0x19a4c116b8d2d0c8), UINT64_C(0x1e376c085141ab53), + UINT64_C(0x2748774cdf8eeb99), UINT64_C(0x34b0bcb5e19b48a8), + UINT64_C(0x391c0cb3c5c95a63), UINT64_C(0x4ed8aa4ae3418acb), + UINT64_C(0x5b9cca4f7763e373), UINT64_C(0x682e6ff3d6b2b8a3), + UINT64_C(0x748f82ee5defb2fc), UINT64_C(0x78a5636f43172f60), + UINT64_C(0x84c87814a1f0ab72), UINT64_C(0x8cc702081a6439ec), + UINT64_C(0x90befffa23631e28), UINT64_C(0xa4506cebde82bde9), + UINT64_C(0xbef9a3f7b2c67915), UINT64_C(0xc67178f2e372532b), + UINT64_C(0xca273eceea26619c), UINT64_C(0xd186b8c721c0c207), + UINT64_C(0xeada7dd6cde0eb1e), UINT64_C(0xf57d4f7fee6ed178), + UINT64_C(0x06f067aa72176fba), UINT64_C(0x0a637dc5a2c898a6), + UINT64_C(0x113f9804bef90dae), UINT64_C(0x1b710b35131c471b), + UINT64_C(0x28db77f523047d84), UINT64_C(0x32caab7b40c72493), + UINT64_C(0x3c9ebe0a15c9bebc), UINT64_C(0x431d67c49c100d4c), + UINT64_C(0x4cc5d4becb3e42b6), UINT64_C(0x597f299cfc657e2a), + UINT64_C(0x5fcb6fab3ad6faec), UINT64_C(0x6c44198c4a475817) +}; + +/* Various logical functions */ + +#define ROR64c(x, y) \ + ( ((((x)&UINT64_C(0xFFFFFFFFFFFFFFFF))>>((uint64_t)(y)&UINT64_C(63))) | \ + ((x)<<((uint64_t)(64-((y)&UINT64_C(63)))))) & UINT64_C(0xFFFFFFFFFFFFFFFF)) + +#define STORE64H(x, y) \ + { (y)[0] = (unsigned char)(((x)>>56)&255); (y)[1] = (unsigned char)(((x)>>48)&255); \ + (y)[2] = (unsigned char)(((x)>>40)&255); (y)[3] = (unsigned char)(((x)>>32)&255); \ + (y)[4] = (unsigned char)(((x)>>24)&255); (y)[5] = (unsigned char)(((x)>>16)&255); \ + (y)[6] = (unsigned char)(((x)>>8)&255); (y)[7] = (unsigned char)((x)&255); } + +#define LOAD64H(x, y) \ + { x = (((uint64_t)((y)[0] & 255))<<56)|(((uint64_t)((y)[1] & 255))<<48) | \ + (((uint64_t)((y)[2] & 255))<<40)|(((uint64_t)((y)[3] & 255))<<32) | \ + (((uint64_t)((y)[4] & 255))<<24)|(((uint64_t)((y)[5] & 255))<<16) | \ + (((uint64_t)((y)[6] & 255))<<8)|(((uint64_t)((y)[7] & 255))); } + + +#define Ch(x,y,z) (z ^ (x & (y ^ z))) +#define Maj(x,y,z) (((x | y) & z) | (x & y)) +#define S(x, n) ROR64c(x, n) +#define R(x, n) (((x) &UINT64_C(0xFFFFFFFFFFFFFFFF))>>((uint64_t)n)) +#define Sigma0(x) (S(x, 28) ^ S(x, 34) ^ S(x, 39)) +#define Sigma1(x) (S(x, 14) ^ S(x, 18) ^ S(x, 41)) +#define Gamma0(x) (S(x, 1) ^ S(x, 8) ^ R(x, 7)) +#define Gamma1(x) (S(x, 19) ^ S(x, 61) ^ R(x, 6)) +#ifndef MIN + #define MIN(x, y) ( ((x)<(y))?(x):(y) ) +#endif + +/* compress 1024-bits */ +static int sha512_compress(sha512_context *md, unsigned char *buf) +{ + uint64_t S[8], W[80], t0, t1; + int i; + + /* copy state into S */ + for (i = 0; i < 8; i++) { + S[i] = md->state[i]; + } + + /* copy the state into 1024-bits into W[0..15] */ + for (i = 0; i < 16; i++) { + LOAD64H(W[i], buf + (8*i)); + } + + /* fill W[16..79] */ + for (i = 16; i < 80; i++) { + W[i] = Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]; + } + +/* Compress */ + #define RND(a,b,c,d,e,f,g,h,i) \ + t0 = h + Sigma1(e) + Ch(e, f, g) + K[i] + W[i]; \ + t1 = Sigma0(a) + Maj(a, b, c);\ + d += t0; \ + h = t0 + t1; + + for (i = 0; i < 80; i += 8) { + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],i+0); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],i+1); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],i+2); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],i+3); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],i+4); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],i+5); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],i+6); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],i+7); + } + + #undef RND + + + + /* feedback */ + for (i = 0; i < 8; i++) { + md->state[i] = md->state[i] + S[i]; + } + + return 0; +} + + +/** + Initialize the hash state + @param md The hash state you wish to initialize + @return 0 if successful +*/ +int sha512_init(sha512_context * md) { + if (md == NULL) return 1; + + md->curlen = 0; + md->length = 0; + md->state[0] = UINT64_C(0x6a09e667f3bcc908); + md->state[1] = UINT64_C(0xbb67ae8584caa73b); + md->state[2] = UINT64_C(0x3c6ef372fe94f82b); + md->state[3] = UINT64_C(0xa54ff53a5f1d36f1); + md->state[4] = UINT64_C(0x510e527fade682d1); + md->state[5] = UINT64_C(0x9b05688c2b3e6c1f); + md->state[6] = UINT64_C(0x1f83d9abfb41bd6b); + md->state[7] = UINT64_C(0x5be0cd19137e2179); + + return 0; +} + +/** + Process a block of memory though the hash + @param md The hash state + @param in The data to hash + @param inlen The length of the data (octets) + @return 0 if successful +*/ +int sha512_update (sha512_context * md, const unsigned char *in, size_t inlen) +{ + size_t n; + size_t i; + int err; + if (md == NULL) return 1; + if (in == NULL) return 1; + if (md->curlen > sizeof(md->buf)) { + return 1; + } + while (inlen > 0) { + if (md->curlen == 0 && inlen >= 128) { + if ((err = sha512_compress (md, (unsigned char *)in)) != 0) { + return err; + } + md->length += 128 * 8; + in += 128; + inlen -= 128; + } else { + n = MIN(inlen, (128 - md->curlen)); + + for (i = 0; i < n; i++) { + md->buf[i + md->curlen] = in[i]; + } + + + md->curlen += n; + in += n; + inlen -= n; + if (md->curlen == 128) { + if ((err = sha512_compress (md, md->buf)) != 0) { + return err; + } + md->length += 8*128; + md->curlen = 0; + } + } + } + return 0; +} + +/** + Terminate the hash to get the digest + @param md The hash state + @param out [out] The destination of the hash (64 bytes) + @return 0 if successful +*/ + int sha512_final(sha512_context * md, unsigned char *out) + { + int i; + + if (md == NULL) return 1; + if (out == NULL) return 1; + + if (md->curlen >= sizeof(md->buf)) { + return 1; + } + + /* increase the length of the message */ + md->length += md->curlen * UINT64_C(8); + + /* append the '1' bit */ + md->buf[md->curlen++] = (unsigned char)0x80; + + /* if the length is currently above 112 bytes we append zeros + * then compress. Then we can fall back to padding zeros and length + * encoding like normal. + */ + if (md->curlen > 112) { + while (md->curlen < 128) { + md->buf[md->curlen++] = (unsigned char)0; + } + sha512_compress(md, md->buf); + md->curlen = 0; + } + + /* pad upto 120 bytes of zeroes + * note: that from 112 to 120 is the 64 MSB of the length. We assume that you won't hash + * > 2^64 bits of data... :-) + */ +while (md->curlen < 120) { + md->buf[md->curlen++] = (unsigned char)0; +} + + /* store length */ +STORE64H(md->length, md->buf+120); +sha512_compress(md, md->buf); + + /* copy output */ +for (i = 0; i < 8; i++) { + STORE64H(md->state[i], out+(8*i)); +} + +return 0; +} + +int sha512(const unsigned char *message, size_t message_len, unsigned char *out) +{ + sha512_context ctx; + int ret; + if ((ret = sha512_init(&ctx))) return ret; + if ((ret = sha512_update(&ctx, message, message_len))) return ret; + if ((ret = sha512_final(&ctx, out))) return ret; + return 0; +} diff --git a/zephcore/lib/ed25519/sha512.h b/zephcore/lib/ed25519/sha512.h new file mode 100644 index 0000000..a34dd5e --- /dev/null +++ b/zephcore/lib/ed25519/sha512.h @@ -0,0 +1,21 @@ +#ifndef SHA512_H +#define SHA512_H + +#include + +#include "fixedint.h" + +/* state */ +typedef struct sha512_context_ { + uint64_t length, state[8]; + size_t curlen; + unsigned char buf[128]; +} sha512_context; + + +int sha512_init(sha512_context * md); +int sha512_final(sha512_context * md, unsigned char *out); +int sha512_update(sha512_context * md, const unsigned char *in, size_t inlen); +int sha512(const unsigned char *message, size_t message_len, unsigned char *out); + +#endif diff --git a/zephcore/lib/ed25519/sign.c b/zephcore/lib/ed25519/sign.c new file mode 100644 index 0000000..463b02b --- /dev/null +++ b/zephcore/lib/ed25519/sign.c @@ -0,0 +1,31 @@ +#include "ed_25519.h" +#include "sha512.h" +#include "ge.h" +#include "sc.h" + + +void ed25519_sign(unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key, const unsigned char *private_key) { + sha512_context hash; + unsigned char hram[64]; + unsigned char r[64]; + ge_p3 R; + + + sha512_init(&hash); + sha512_update(&hash, private_key + 32, 32); + sha512_update(&hash, message, message_len); + sha512_final(&hash, r); + + sc_reduce(r); + ge_scalarmult_base(&R, r); + ge_p3_tobytes(signature, &R); + + sha512_init(&hash); + sha512_update(&hash, signature, 32); + sha512_update(&hash, public_key, 32); + sha512_update(&hash, message, message_len); + sha512_final(&hash, hram); + + sc_reduce(hram); + sc_muladd(signature + 32, hram, private_key, r); +} diff --git a/zephcore/lib/ed25519/verify.c b/zephcore/lib/ed25519/verify.c new file mode 100644 index 0000000..4d8de25 --- /dev/null +++ b/zephcore/lib/ed25519/verify.c @@ -0,0 +1,77 @@ +#include "ed_25519.h" +#include "sha512.h" +#include "ge.h" +#include "sc.h" + +static int consttime_equal(const unsigned char *x, const unsigned char *y) { + unsigned char r = 0; + + r = x[0] ^ y[0]; + #define F(i) r |= x[i] ^ y[i] + F(1); + F(2); + F(3); + F(4); + F(5); + F(6); + F(7); + F(8); + F(9); + F(10); + F(11); + F(12); + F(13); + F(14); + F(15); + F(16); + F(17); + F(18); + F(19); + F(20); + F(21); + F(22); + F(23); + F(24); + F(25); + F(26); + F(27); + F(28); + F(29); + F(30); + F(31); + #undef F + + return !r; +} + +int ed25519_verify(const unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key) { + unsigned char h[64]; + unsigned char checker[32]; + sha512_context hash; + ge_p3 A; + ge_p2 R; + + if (signature[63] & 224) { + return 0; + } + + if (ge_frombytes_negate_vartime(&A, public_key) != 0) { + return 0; + } + + sha512_init(&hash); + sha512_update(&hash, signature, 32); + sha512_update(&hash, public_key, 32); + sha512_update(&hash, message, message_len); + sha512_final(&hash, h); + + sc_reduce(h); + ge_double_scalarmult_vartime(&R, h, &A, signature + 32); + ge_tobytes(checker, &R); + + if (!consttime_equal(checker, signature)) { + return 0; + } + + return 1; +} diff --git a/zephcore/patches/modules/lib/loramac-node/src/radio/sx126x/radio.c b/zephcore/patches/modules/lib/loramac-node/src/radio/sx126x/radio.c new file mode 100644 index 0000000..a15dd07 --- /dev/null +++ b/zephcore/patches/modules/lib/loramac-node/src/radio/sx126x/radio.c @@ -0,0 +1,1413 @@ +/*! + * \file radio.c + * + * \brief Radio driver API definition + * + * \copyright Revised BSD License, see section \ref LICENSE. + * + * \code + * ______ _ + * / _____) _ | | + * ( (____ _____ ____ _| |_ _____ ____| |__ + * \____ \| ___ | (_ _) ___ |/ ___) _ \ + * _____) ) ____| | | || |_| ____( (___| | | | + * (______/|_____)_|_|_| \__)_____)\____)_| |_| + * (C)2013-2017 Semtech + * + * \endcode + * + * \author Miguel Luis ( Semtech ) + * + * \author Gregory Cristian ( Semtech ) + */ +#include +#include +#include "utilities.h" +#include "timer.h" +#include "delay.h" +#include "radio.h" +#include "sx126x.h" +#include "sx126x-board.h" +#include "board.h" + +/*! + * \brief Initializes the radio + * + * \param [IN] events Structure containing the driver callback functions + */ +void RadioInit( RadioEvents_t *events ); + +/*! + * Return current radio status + * + * \param status Radio status.[RF_IDLE, RF_RX_RUNNING, RF_TX_RUNNING] + */ +RadioState_t RadioGetStatus( void ); + +/*! + * \brief Configures the radio with the given modem + * + * \param [IN] modem Modem to be used [0: FSK, 1: LoRa] + */ +void RadioSetModem( RadioModems_t modem ); + +/*! + * \brief Sets the channel frequency + * + * \param [IN] freq Channel RF frequency + */ +void RadioSetChannel( uint32_t freq ); + +/*! + * \brief Checks if the channel is free for the given time + * + * \remark The FSK modem is always used for this task as we can select the Rx bandwidth at will. + * + * \param [IN] freq Channel RF frequency in Hertz + * \param [IN] rxBandwidth Rx bandwidth in Hertz + * \param [IN] rssiThresh RSSI threshold in dBm + * \param [IN] maxCarrierSenseTime Max time in milliseconds while the RSSI is measured + * + * \retval isFree [true: Channel is free, false: Channel is not free] + */ +bool RadioIsChannelFree( uint32_t freq, uint32_t rxBandwidth, int16_t rssiThresh, uint32_t maxCarrierSenseTime ); + +/*! + * \brief Generates a 32 bits random value based on the RSSI readings + * + * \remark This function sets the radio in LoRa modem mode and disables + * all interrupts. + * After calling this function either Radio.SetRxConfig or + * Radio.SetTxConfig functions must be called. + * + * \retval randomValue 32 bits random value + */ +uint32_t RadioRandom( void ); + +/*! + * \brief Sets the reception parameters + * + * \param [IN] modem Radio modem to be used [0: FSK, 1: LoRa] + * \param [IN] bandwidth Sets the bandwidth + * FSK : >= 2600 and <= 250000 Hz + * LoRa: [0: 125 kHz, 1: 250 kHz, + * 2: 500 kHz, 3: Reserved] + * \param [IN] datarate Sets the Datarate + * FSK : 600..300000 bits/s + * LoRa: [6: 64, 7: 128, 8: 256, 9: 512, + * 10: 1024, 11: 2048, 12: 4096 chips] + * \param [IN] coderate Sets the coding rate (LoRa only) + * FSK : N/A ( set to 0 ) + * LoRa: [1: 4/5, 2: 4/6, 3: 4/7, 4: 4/8] + * \param [IN] bandwidthAfc Sets the AFC Bandwidth (FSK only) + * FSK : >= 2600 and <= 250000 Hz + * LoRa: N/A ( set to 0 ) + * \param [IN] preambleLen Sets the Preamble length + * FSK : Number of bytes + * LoRa: Length in symbols (the hardware adds 4 more symbols) + * \param [IN] symbTimeout Sets the RxSingle timeout value + * FSK : timeout in number of bytes + * LoRa: timeout in symbols + * \param [IN] fixLen Fixed length packets [0: variable, 1: fixed] + * \param [IN] payloadLen Sets payload length when fixed length is used + * \param [IN] crcOn Enables/Disables the CRC [0: OFF, 1: ON] + * \param [IN] FreqHopOn Enables disables the intra-packet frequency hopping + * FSK : N/A ( set to 0 ) + * LoRa: [0: OFF, 1: ON] + * \param [IN] HopPeriod Number of symbols between each hop + * FSK : N/A ( set to 0 ) + * LoRa: Number of symbols + * \param [IN] iqInverted Inverts IQ signals (LoRa only) + * FSK : N/A ( set to 0 ) + * LoRa: [0: not inverted, 1: inverted] + * \param [IN] rxContinuous Sets the reception in continuous mode + * [false: single mode, true: continuous mode] + */ +void RadioSetRxConfig( RadioModems_t modem, uint32_t bandwidth, + uint32_t datarate, uint8_t coderate, + uint32_t bandwidthAfc, uint16_t preambleLen, + uint16_t symbTimeout, bool fixLen, + uint8_t payloadLen, + bool crcOn, bool FreqHopOn, uint8_t HopPeriod, + bool iqInverted, bool rxContinuous ); + +/*! + * \brief Sets the transmission parameters + * + * \param [IN] modem Radio modem to be used [0: FSK, 1: LoRa] + * \param [IN] power Sets the output power [dBm] + * \param [IN] fdev Sets the frequency deviation (FSK only) + * FSK : [Hz] + * LoRa: 0 + * \param [IN] bandwidth Sets the bandwidth (LoRa only) + * FSK : 0 + * LoRa: [0: 125 kHz, 1: 250 kHz, + * 2: 500 kHz, 3: Reserved] + * \param [IN] datarate Sets the Datarate + * FSK : 600..300000 bits/s + * LoRa: [6: 64, 7: 128, 8: 256, 9: 512, + * 10: 1024, 11: 2048, 12: 4096 chips] + * \param [IN] coderate Sets the coding rate (LoRa only) + * FSK : N/A ( set to 0 ) + * LoRa: [1: 4/5, 2: 4/6, 3: 4/7, 4: 4/8] + * \param [IN] preambleLen Sets the preamble length + * FSK : Number of bytes + * LoRa: Length in symbols (the hardware adds 4 more symbols) + * \param [IN] fixLen Fixed length packets [0: variable, 1: fixed] + * \param [IN] crcOn Enables disables the CRC [0: OFF, 1: ON] + * \param [IN] FreqHopOn Enables disables the intra-packet frequency hopping + * FSK : N/A ( set to 0 ) + * LoRa: [0: OFF, 1: ON] + * \param [IN] HopPeriod Number of symbols between each hop + * FSK : N/A ( set to 0 ) + * LoRa: Number of symbols + * \param [IN] iqInverted Inverts IQ signals (LoRa only) + * FSK : N/A ( set to 0 ) + * LoRa: [0: not inverted, 1: inverted] + * \param [IN] timeout Transmission timeout [ms] + */ +void RadioSetTxConfig( RadioModems_t modem, int8_t power, uint32_t fdev, + uint32_t bandwidth, uint32_t datarate, + uint8_t coderate, uint16_t preambleLen, + bool fixLen, bool crcOn, bool FreqHopOn, + uint8_t HopPeriod, bool iqInverted, uint32_t timeout ); + +/*! + * \brief Checks if the given RF frequency is supported by the hardware + * + * \param [IN] frequency RF frequency to be checked + * \retval isSupported [true: supported, false: unsupported] + */ +bool RadioCheckRfFrequency( uint32_t frequency ); + +/*! + * \brief Computes the packet time on air in ms for the given payload + * + * \Remark Can only be called once SetRxConfig or SetTxConfig have been called + * + * \param [IN] modem Radio modem to be used [0: FSK, 1: LoRa] + * \param [IN] bandwidth Sets the bandwidth + * FSK : >= 2600 and <= 250000 Hz + * LoRa: [0: 125 kHz, 1: 250 kHz, + * 2: 500 kHz, 3: Reserved] + * \param [IN] datarate Sets the Datarate + * FSK : 600..300000 bits/s + * LoRa: [6: 64, 7: 128, 8: 256, 9: 512, + * 10: 1024, 11: 2048, 12: 4096 chips] + * \param [IN] coderate Sets the coding rate (LoRa only) + * FSK : N/A ( set to 0 ) + * LoRa: [1: 4/5, 2: 4/6, 3: 4/7, 4: 4/8] + * \param [IN] preambleLen Sets the Preamble length + * FSK : Number of bytes + * LoRa: Length in symbols (the hardware adds 4 more symbols) + * \param [IN] fixLen Fixed length packets [0: variable, 1: fixed] + * \param [IN] payloadLen Sets payload length when fixed length is used + * \param [IN] crcOn Enables/Disables the CRC [0: OFF, 1: ON] + * + * \retval airTime Computed airTime (ms) for the given packet payload length + */ +uint32_t RadioTimeOnAir( RadioModems_t modem, uint32_t bandwidth, + uint32_t datarate, uint8_t coderate, + uint16_t preambleLen, bool fixLen, uint8_t payloadLen, + bool crcOn ); + +/*! + * \brief Sends the buffer of size. Prepares the packet to be sent and sets + * the radio in transmission + * + * \param [IN]: buffer Buffer pointer + * \param [IN]: size Buffer size + */ +void RadioSend( uint8_t *buffer, uint8_t size ); + +/*! + * \brief Sets the radio in sleep mode + */ +void RadioSleep( void ); + +/*! + * \brief Sets the radio in standby mode + */ +void RadioStandby( void ); + +/*! + * \brief Sets the radio in reception mode for the given time + * \param [IN] timeout Reception timeout [ms] + * [0: continuous, others timeout] + */ +void RadioRx( uint32_t timeout ); + +/*! + * \brief Start a Channel Activity Detection + */ +void RadioStartCad( void ); + +/*! + * \brief Sets the radio in continuous wave transmission mode + * + * \param [IN]: freq Channel RF frequency + * \param [IN]: power Sets the output power [dBm] + * \param [IN]: time Transmission mode timeout [s] + */ +void RadioSetTxContinuousWave( uint32_t freq, int8_t power, uint16_t time ); + +/*! + * \brief Reads the current RSSI value + * + * \retval rssiValue Current RSSI value in [dBm] + */ +int16_t RadioRssi( RadioModems_t modem ); + +/*! + * \brief Writes the radio register at the specified address + * + * \param [IN]: addr Register address + * \param [IN]: data New register value + */ +void RadioWrite( uint32_t addr, uint8_t data ); + +/*! + * \brief Reads the radio register at the specified address + * + * \param [IN]: addr Register address + * \retval data Register value + */ +uint8_t RadioRead( uint32_t addr ); + +/*! + * \brief Writes multiple radio registers starting at address + * + * \param [IN] addr First Radio register address + * \param [IN] buffer Buffer containing the new register's values + * \param [IN] size Number of registers to be written + */ +void RadioWriteBuffer( uint32_t addr, uint8_t *buffer, uint8_t size ); + +/*! + * \brief Reads multiple radio registers starting at address + * + * \param [IN] addr First Radio register address + * \param [OUT] buffer Buffer where to copy the registers data + * \param [IN] size Number of registers to be read + */ +void RadioReadBuffer( uint32_t addr, uint8_t *buffer, uint8_t size ); + +/*! + * \brief Sets the maximum payload length. + * + * \param [IN] modem Radio modem to be used [0: FSK, 1: LoRa] + * \param [IN] max Maximum payload length in bytes + */ +void RadioSetMaxPayloadLength( RadioModems_t modem, uint8_t max ); + +/*! + * \brief Sets the network to public or private. Updates the sync byte. + * + * \remark Applies to LoRa modem only + * + * \param [IN] enable if true, it enables a public network + */ +void RadioSetPublicNetwork( bool enable ); + +/*! + * \brief Gets the time required for the board plus radio to get out of sleep.[ms] + * + * \retval time Radio plus board wakeup time in ms. + */ +uint32_t RadioGetWakeupTime( void ); + +/*! + * \brief Process radio irq + */ +void RadioIrqProcess( void ); + +/*! + * \brief Sets the radio in reception mode with Max LNA gain for the given time + * \param [IN] timeout Reception timeout [ms] + * [0: continuous, others timeout] + */ +void RadioRxBoosted( uint32_t timeout ); + +/*! + * \brief Sets the Rx duty cycle management parameters + * + * \param [in] rxTime Structure describing reception timeout value + * \param [in] sleepTime Structure describing sleep timeout value + */ +void RadioSetRxDutyCycle( uint32_t rxTime, uint32_t sleepTime ); + +/*! + * \brief Add a register to the retention list + * + * \param [in] registerAddress The address of the register to be kept in retention + */ +void RadioAddRegisterToRetentionList( uint16_t registerAddress ); + +/*! + * Radio driver structure initialization + */ +const struct Radio_s Radio = +{ + RadioInit, + RadioGetStatus, + RadioSetModem, + RadioSetChannel, + RadioIsChannelFree, + RadioRandom, + RadioSetRxConfig, + RadioSetTxConfig, + RadioCheckRfFrequency, + RadioTimeOnAir, + RadioSend, + RadioSleep, + RadioStandby, + RadioRx, + RadioStartCad, + RadioSetTxContinuousWave, + RadioRssi, + RadioWrite, + RadioRead, + RadioWriteBuffer, + RadioReadBuffer, + RadioSetMaxPayloadLength, + RadioSetPublicNetwork, + RadioGetWakeupTime, + RadioIrqProcess, + // Available on SX126x only + RadioRxBoosted, + RadioSetRxDutyCycle +}; + +/* + * Local types definition + */ + + + /*! + * FSK bandwidth definition + */ +typedef struct +{ + uint32_t bandwidth; + uint8_t RegValue; +}FskBandwidth_t; + +/*! + * Precomputed FSK bandwidth registers values + */ +const FskBandwidth_t FskBandwidths[] = +{ + { 4800 , 0x1F }, + { 5800 , 0x17 }, + { 7300 , 0x0F }, + { 9700 , 0x1E }, + { 11700 , 0x16 }, + { 14600 , 0x0E }, + { 19500 , 0x1D }, + { 23400 , 0x15 }, + { 29300 , 0x0D }, + { 39000 , 0x1C }, + { 46900 , 0x14 }, + { 58600 , 0x0C }, + { 78200 , 0x1B }, + { 93800 , 0x13 }, + { 117300, 0x0B }, + { 156200, 0x1A }, + { 187200, 0x12 }, + { 234300, 0x0A }, + { 312000, 0x19 }, + { 373600, 0x11 }, + { 467000, 0x09 }, + { 500000, 0x00 }, // Invalid Bandwidth +}; + +/* + * ZephCore patch: restore full 10-element Bandwidths array. + * Upstream Zephyr reduced this to {125, 250, 500} but the Zephyr LoRa API + * now defines enums for all bandwidths (BW_7_KHZ through BW_500_KHZ). + * sx12xx_common.c maps these enums to indices into this array, so all 10 + * entries must be present to avoid out-of-bounds access. + */ +const RadioLoRaBandwidths_t Bandwidths[] = +{ + LORA_BW_007, + LORA_BW_010, + LORA_BW_015, + LORA_BW_020, + LORA_BW_031, + LORA_BW_041, + LORA_BW_062, + LORA_BW_125, + LORA_BW_250, + LORA_BW_500, +}; + +uint8_t MaxPayloadLength = 0xFF; + +uint32_t TxTimeout = 0; +uint32_t RxTimeout = 0; + +bool RxContinuous = false; + + +PacketStatus_t RadioPktStatus; +uint8_t RadioRxPayload[255]; + +bool IrqFired = false; + +/* + * SX126x DIO IRQ callback functions prototype + */ + +/*! + * \brief DIO 0 IRQ callback + */ +void RadioOnDioIrq( void* context ); + +/*! + * \brief Tx timeout timer callback + */ +void RadioOnTxTimeoutIrq( void* context ); + +/*! + * \brief Rx timeout timer callback + */ +void RadioOnRxTimeoutIrq( void* context ); + +/* + * Private global variables + */ + + +/*! + * Holds the current network type for the radio + */ +typedef struct +{ + bool Previous; + bool Current; +}RadioPublicNetwork_t; + +static RadioPublicNetwork_t RadioPublicNetwork = { false }; + +/*! + * Radio callbacks variable + */ +static RadioEvents_t* RadioEvents; + +/* + * Public global variables + */ + +/*! + * Radio hardware and global parameters + */ +SX126x_t SX126x; + +/*! + * Tx and Rx timers + */ +TimerEvent_t TxTimeoutTimer; +TimerEvent_t RxTimeoutTimer; + +/*! + * Returns the known FSK bandwidth registers value + * + * \param [IN] bandwidth Bandwidth value in Hz + * \retval regValue Bandwidth register value. + */ +static uint8_t RadioGetFskBandwidthRegValue( uint32_t bandwidth ) +{ + uint8_t i; + + if( bandwidth == 0 ) + { + return( 0x1F ); + } + + for( i = 0; i < ( sizeof( FskBandwidths ) / sizeof( FskBandwidth_t ) ) - 1; i++ ) + { + if( ( bandwidth >= FskBandwidths[i].bandwidth ) && ( bandwidth < FskBandwidths[i + 1].bandwidth ) ) + { + return FskBandwidths[i+1].RegValue; + } + } + // ERROR: Value not found + while( 1 ); +} + +void RadioInit( RadioEvents_t *events ) +{ + RadioEvents = events; + + SX126xInit( RadioOnDioIrq ); + SX126xSetStandby( STDBY_RC ); + SX126xSetRegulatorMode( USE_DCDC ); + + SX126xSetBufferBaseAddress( 0x00, 0x00 ); + SX126xSetTxParams( 0, RADIO_RAMP_200_US ); + SX126xSetDioIrqParams( IRQ_RADIO_ALL, IRQ_RADIO_ALL, IRQ_RADIO_NONE, IRQ_RADIO_NONE ); + + // Add registers to the retention list (4 is the maximum possible number) + RadioAddRegisterToRetentionList( REG_RX_GAIN ); + RadioAddRegisterToRetentionList( REG_TX_MODULATION ); + + // Initialize driver timeout timers + TimerInit( &TxTimeoutTimer, RadioOnTxTimeoutIrq ); + TimerInit( &RxTimeoutTimer, RadioOnRxTimeoutIrq ); + + IrqFired = false; +} + +RadioState_t RadioGetStatus( void ) +{ + switch( SX126xGetOperatingMode( ) ) + { + case MODE_TX: + return RF_TX_RUNNING; + case MODE_RX: + return RF_RX_RUNNING; + case MODE_CAD: + return RF_CAD; + default: + return RF_IDLE; + } +} + +void RadioSetModem( RadioModems_t modem ) +{ + switch( modem ) + { + default: + case MODEM_FSK: + SX126xSetPacketType( PACKET_TYPE_GFSK ); + // When switching to GFSK mode the LoRa SyncWord register value is reset + // Thus, we also reset the RadioPublicNetwork variable + RadioPublicNetwork.Current = false; + break; + case MODEM_LORA: + SX126xSetPacketType( PACKET_TYPE_LORA ); + // Public/Private network register is reset when switching modems + if( RadioPublicNetwork.Current != RadioPublicNetwork.Previous ) + { + RadioPublicNetwork.Current = RadioPublicNetwork.Previous; + RadioSetPublicNetwork( RadioPublicNetwork.Current ); + } + break; + } +} + +void RadioSetChannel( uint32_t freq ) +{ + SX126xSetRfFrequency( freq ); +} + +bool RadioIsChannelFree( uint32_t freq, uint32_t rxBandwidth, int16_t rssiThresh, uint32_t maxCarrierSenseTime ) +{ + bool status = true; + int16_t rssi = 0; + uint32_t carrierSenseTime = 0; + + RadioSetModem( MODEM_FSK ); + + RadioSetChannel( freq ); + + // Set Rx bandwidth. Other parameters are not used. + RadioSetRxConfig( MODEM_FSK, rxBandwidth, 600, 0, rxBandwidth, 3, 0, false, + 0, false, 0, 0, false, true ); + RadioRx( 0 ); + + DelayMs( 1 ); + + carrierSenseTime = TimerGetCurrentTime( ); + + // Perform carrier sense for maxCarrierSenseTime + while( TimerGetElapsedTime( carrierSenseTime ) < maxCarrierSenseTime ) + { + rssi = RadioRssi( MODEM_FSK ); + + if( rssi > rssiThresh ) + { + status = false; + break; + } + } + RadioSleep( ); + return status; +} + +uint32_t RadioRandom( void ) +{ + uint32_t rnd = 0; + + /* + * Radio setup for random number generation + */ + // Set LoRa modem ON + RadioSetModem( MODEM_LORA ); + + // Disable LoRa modem interrupts + SX126xSetDioIrqParams( IRQ_RADIO_NONE, IRQ_RADIO_NONE, IRQ_RADIO_NONE, IRQ_RADIO_NONE ); + + rnd = SX126xGetRandom( ); + + return rnd; +} + +void RadioSetRxConfig( RadioModems_t modem, uint32_t bandwidth, + uint32_t datarate, uint8_t coderate, + uint32_t bandwidthAfc, uint16_t preambleLen, + uint16_t symbTimeout, bool fixLen, + uint8_t payloadLen, + bool crcOn, bool freqHopOn, uint8_t hopPeriod, + bool iqInverted, bool rxContinuous ) +{ + + RxContinuous = rxContinuous; + if( rxContinuous == true ) + { + symbTimeout = 0; + } + if( fixLen == true ) + { + MaxPayloadLength = payloadLen; + } + else + { + MaxPayloadLength = 0xFF; + } + + switch( modem ) + { + case MODEM_FSK: + SX126xSetStopRxTimerOnPreambleDetect( false ); + SX126x.ModulationParams.PacketType = PACKET_TYPE_GFSK; + + SX126x.ModulationParams.Params.Gfsk.BitRate = datarate; + SX126x.ModulationParams.Params.Gfsk.ModulationShaping = MOD_SHAPING_G_BT_1; + SX126x.ModulationParams.Params.Gfsk.Bandwidth = RadioGetFskBandwidthRegValue( bandwidth << 1 ); // SX126x badwidth is double sided + + SX126x.PacketParams.PacketType = PACKET_TYPE_GFSK; + SX126x.PacketParams.Params.Gfsk.PreambleLength = ( preambleLen << 3 ); // convert byte into bit + SX126x.PacketParams.Params.Gfsk.PreambleMinDetect = RADIO_PREAMBLE_DETECTOR_08_BITS; + SX126x.PacketParams.Params.Gfsk.SyncWordLength = 3 << 3; // convert byte into bit + SX126x.PacketParams.Params.Gfsk.AddrComp = RADIO_ADDRESSCOMP_FILT_OFF; + SX126x.PacketParams.Params.Gfsk.HeaderType = ( fixLen == true ) ? RADIO_PACKET_FIXED_LENGTH : RADIO_PACKET_VARIABLE_LENGTH; + SX126x.PacketParams.Params.Gfsk.PayloadLength = MaxPayloadLength; + if( crcOn == true ) + { + SX126x.PacketParams.Params.Gfsk.CrcLength = RADIO_CRC_2_BYTES_CCIT; + } + else + { + SX126x.PacketParams.Params.Gfsk.CrcLength = RADIO_CRC_OFF; + } + SX126x.PacketParams.Params.Gfsk.DcFree = RADIO_DC_FREEWHITENING; + + RadioStandby( ); + RadioSetModem( ( SX126x.ModulationParams.PacketType == PACKET_TYPE_GFSK ) ? MODEM_FSK : MODEM_LORA ); + SX126xSetModulationParams( &SX126x.ModulationParams ); + SX126xSetPacketParams( &SX126x.PacketParams ); + SX126xSetSyncWord( ( uint8_t[] ){ 0xC1, 0x94, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00 } ); + SX126xSetWhiteningSeed( 0x01FF ); + + RxTimeout = ( uint32_t )symbTimeout * 8000UL / datarate; + break; + + case MODEM_LORA: + SX126xSetStopRxTimerOnPreambleDetect( false ); + SX126x.ModulationParams.PacketType = PACKET_TYPE_LORA; + SX126x.ModulationParams.Params.LoRa.SpreadingFactor = ( RadioLoRaSpreadingFactors_t )datarate; + SX126x.ModulationParams.Params.LoRa.Bandwidth = Bandwidths[bandwidth]; + SX126x.ModulationParams.Params.LoRa.CodingRate = ( RadioLoRaCodingRates_t )coderate; + + /* + * ZephCore patch: LDRO check updated for 10-element Bandwidths[]. + * Enable LDRO when symbol duration > 16ms. + * Old indices (0=125k,1=250k) → new (7=125k,8=250k,6=62.5k, etc.) + */ + if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) || + ( ( bandwidth == 8 ) && ( datarate == 12 ) ) || + ( ( bandwidth <= 6 ) && ( datarate >= 10 ) ) ) + { + SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x01; + } + else + { + SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x00; + } + + SX126x.PacketParams.PacketType = PACKET_TYPE_LORA; + + if( ( SX126x.ModulationParams.Params.LoRa.SpreadingFactor == LORA_SF5 ) || + ( SX126x.ModulationParams.Params.LoRa.SpreadingFactor == LORA_SF6 ) ) + { + if( preambleLen < 12 ) + { + SX126x.PacketParams.Params.LoRa.PreambleLength = 12; + } + else + { + SX126x.PacketParams.Params.LoRa.PreambleLength = preambleLen; + } + } + else + { + SX126x.PacketParams.Params.LoRa.PreambleLength = preambleLen; + } + + SX126x.PacketParams.Params.LoRa.HeaderType = ( RadioLoRaPacketLengthsMode_t )fixLen; + + SX126x.PacketParams.Params.LoRa.PayloadLength = MaxPayloadLength; + SX126x.PacketParams.Params.LoRa.CrcMode = ( RadioLoRaCrcModes_t )crcOn; + SX126x.PacketParams.Params.LoRa.InvertIQ = ( RadioLoRaIQModes_t )iqInverted; + + RadioStandby( ); + RadioSetModem( ( SX126x.ModulationParams.PacketType == PACKET_TYPE_GFSK ) ? MODEM_FSK : MODEM_LORA ); + SX126xSetModulationParams( &SX126x.ModulationParams ); + SX126xSetPacketParams( &SX126x.PacketParams ); + SX126xSetLoRaSymbNumTimeout( symbTimeout ); + + // WORKAROUND - Optimizing the Inverted IQ Operation, see DS_SX1261-2_V1.2 datasheet chapter 15.4 + if( SX126x.PacketParams.Params.LoRa.InvertIQ == LORA_IQ_INVERTED ) + { + SX126xWriteRegister( REG_IQ_POLARITY, SX126xReadRegister( REG_IQ_POLARITY ) & ~( 1 << 2 ) ); + } + else + { + SX126xWriteRegister( REG_IQ_POLARITY, SX126xReadRegister( REG_IQ_POLARITY ) | ( 1 << 2 ) ); + } + // WORKAROUND END + + // Timeout Max, Timeout handled directly in SetRx function + RxTimeout = 0xFFFF; + + break; + } +} + +void RadioSetTxConfig( RadioModems_t modem, int8_t power, uint32_t fdev, + uint32_t bandwidth, uint32_t datarate, + uint8_t coderate, uint16_t preambleLen, + bool fixLen, bool crcOn, bool freqHopOn, + uint8_t hopPeriod, bool iqInverted, uint32_t timeout ) +{ + + switch( modem ) + { + case MODEM_FSK: + SX126x.ModulationParams.PacketType = PACKET_TYPE_GFSK; + SX126x.ModulationParams.Params.Gfsk.BitRate = datarate; + + SX126x.ModulationParams.Params.Gfsk.ModulationShaping = MOD_SHAPING_G_BT_1; + SX126x.ModulationParams.Params.Gfsk.Bandwidth = RadioGetFskBandwidthRegValue( bandwidth << 1 ); // SX126x badwidth is double sided + SX126x.ModulationParams.Params.Gfsk.Fdev = fdev; + + SX126x.PacketParams.PacketType = PACKET_TYPE_GFSK; + SX126x.PacketParams.Params.Gfsk.PreambleLength = ( preambleLen << 3 ); // convert byte into bit + SX126x.PacketParams.Params.Gfsk.PreambleMinDetect = RADIO_PREAMBLE_DETECTOR_08_BITS; + SX126x.PacketParams.Params.Gfsk.SyncWordLength = 3 << 3 ; // convert byte into bit + SX126x.PacketParams.Params.Gfsk.AddrComp = RADIO_ADDRESSCOMP_FILT_OFF; + SX126x.PacketParams.Params.Gfsk.HeaderType = ( fixLen == true ) ? RADIO_PACKET_FIXED_LENGTH : RADIO_PACKET_VARIABLE_LENGTH; + + if( crcOn == true ) + { + SX126x.PacketParams.Params.Gfsk.CrcLength = RADIO_CRC_2_BYTES_CCIT; + } + else + { + SX126x.PacketParams.Params.Gfsk.CrcLength = RADIO_CRC_OFF; + } + SX126x.PacketParams.Params.Gfsk.DcFree = RADIO_DC_FREEWHITENING; + + RadioStandby( ); + RadioSetModem( ( SX126x.ModulationParams.PacketType == PACKET_TYPE_GFSK ) ? MODEM_FSK : MODEM_LORA ); + SX126xSetModulationParams( &SX126x.ModulationParams ); + SX126xSetPacketParams( &SX126x.PacketParams ); + SX126xSetSyncWord( ( uint8_t[] ){ 0xC1, 0x94, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00 } ); + SX126xSetWhiteningSeed( 0x01FF ); + break; + + case MODEM_LORA: + SX126x.ModulationParams.PacketType = PACKET_TYPE_LORA; + SX126x.ModulationParams.Params.LoRa.SpreadingFactor = ( RadioLoRaSpreadingFactors_t ) datarate; + SX126x.ModulationParams.Params.LoRa.Bandwidth = Bandwidths[bandwidth]; + SX126x.ModulationParams.Params.LoRa.CodingRate= ( RadioLoRaCodingRates_t )coderate; + + /* ZephCore patch: LDRO check updated for 10-element Bandwidths[] */ + if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) || + ( ( bandwidth == 8 ) && ( datarate == 12 ) ) || + ( ( bandwidth <= 6 ) && ( datarate >= 10 ) ) ) + { + SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x01; + } + else + { + SX126x.ModulationParams.Params.LoRa.LowDatarateOptimize = 0x00; + } + + SX126x.PacketParams.PacketType = PACKET_TYPE_LORA; + + if( ( SX126x.ModulationParams.Params.LoRa.SpreadingFactor == LORA_SF5 ) || + ( SX126x.ModulationParams.Params.LoRa.SpreadingFactor == LORA_SF6 ) ) + { + if( preambleLen < 12 ) + { + SX126x.PacketParams.Params.LoRa.PreambleLength = 12; + } + else + { + SX126x.PacketParams.Params.LoRa.PreambleLength = preambleLen; + } + } + else + { + SX126x.PacketParams.Params.LoRa.PreambleLength = preambleLen; + } + + SX126x.PacketParams.Params.LoRa.HeaderType = ( RadioLoRaPacketLengthsMode_t )fixLen; + SX126x.PacketParams.Params.LoRa.PayloadLength = MaxPayloadLength; + SX126x.PacketParams.Params.LoRa.CrcMode = ( RadioLoRaCrcModes_t )crcOn; + SX126x.PacketParams.Params.LoRa.InvertIQ = ( RadioLoRaIQModes_t )iqInverted; + + RadioStandby( ); + RadioSetModem( ( SX126x.ModulationParams.PacketType == PACKET_TYPE_GFSK ) ? MODEM_FSK : MODEM_LORA ); + SX126xSetModulationParams( &SX126x.ModulationParams ); + SX126xSetPacketParams( &SX126x.PacketParams ); + break; + } + + // WORKAROUND - Modulation Quality with 500 kHz LoRa Bandwidth, see DS_SX1261-2_V1.2 datasheet chapter 15.1 + if( ( modem == MODEM_LORA ) && ( SX126x.ModulationParams.Params.LoRa.Bandwidth == LORA_BW_500 ) ) + { + SX126xWriteRegister( REG_TX_MODULATION, SX126xReadRegister( REG_TX_MODULATION ) & ~( 1 << 2 ) ); + } + else + { + SX126xWriteRegister( REG_TX_MODULATION, SX126xReadRegister( REG_TX_MODULATION ) | ( 1 << 2 ) ); + } + // WORKAROUND END + + SX126xSetRfTxPower( power ); + TxTimeout = timeout; +} + +bool RadioCheckRfFrequency( uint32_t frequency ) +{ + return true; +} + +static uint32_t RadioGetLoRaBandwidthInHz( RadioLoRaBandwidths_t bw ) +{ + uint32_t bandwidthInHz = 0; + + switch( bw ) + { + case LORA_BW_007: + bandwidthInHz = 7812UL; + break; + case LORA_BW_010: + bandwidthInHz = 10417UL; + break; + case LORA_BW_015: + bandwidthInHz = 15625UL; + break; + case LORA_BW_020: + bandwidthInHz = 20833UL; + break; + case LORA_BW_031: + bandwidthInHz = 31250UL; + break; + case LORA_BW_041: + bandwidthInHz = 41667UL; + break; + case LORA_BW_062: + bandwidthInHz = 62500UL; + break; + case LORA_BW_125: + bandwidthInHz = 125000UL; + break; + case LORA_BW_250: + bandwidthInHz = 250000UL; + break; + case LORA_BW_500: + bandwidthInHz = 500000UL; + break; + } + + return bandwidthInHz; +} + +static uint32_t RadioGetGfskTimeOnAirNumerator( uint32_t datarate, uint8_t coderate, + uint16_t preambleLen, bool fixLen, uint8_t payloadLen, + bool crcOn ) +{ + const RadioAddressComp_t addrComp = RADIO_ADDRESSCOMP_FILT_OFF; + const uint8_t syncWordLength = 3; + + return ( preambleLen << 3 ) + + ( ( fixLen == false ) ? 8 : 0 ) + + ( syncWordLength << 3 ) + + ( ( payloadLen + + ( addrComp == RADIO_ADDRESSCOMP_FILT_OFF ? 0 : 1 ) + + ( ( crcOn == true ) ? 2 : 0 ) + ) << 3 + ); +} + +static uint32_t RadioGetLoRaTimeOnAirNumerator( uint32_t bandwidth, + uint32_t datarate, uint8_t coderate, + uint16_t preambleLen, bool fixLen, uint8_t payloadLen, + bool crcOn ) +{ + int32_t crDenom = coderate + 4; + bool lowDatareOptimize = false; + + // Ensure that the preamble length is at least 12 symbols when using SF5 or + // SF6 + if( ( datarate == 5 ) || ( datarate == 6 ) ) + { + if( preambleLen < 12 ) + { + preambleLen = 12; + } + } + + /* ZephCore patch: LDRO check updated for 10-element Bandwidths[] */ + if( ( ( bandwidth == 7 ) && ( ( datarate == 11 ) || ( datarate == 12 ) ) ) || + ( ( bandwidth == 8 ) && ( datarate == 12 ) ) || + ( ( bandwidth <= 6 ) && ( datarate >= 10 ) ) ) + { + lowDatareOptimize = true; + } + + int32_t ceilDenominator; + int32_t ceilNumerator = ( payloadLen << 3 ) + + ( crcOn ? 16 : 0 ) - + ( 4 * datarate ) + + ( fixLen ? 0 : 20 ); + + if( datarate <= 6 ) + { + ceilDenominator = 4 * datarate; + } + else + { + ceilNumerator += 8; + + if( lowDatareOptimize == true ) + { + ceilDenominator = 4 * ( datarate - 2 ); + } + else + { + ceilDenominator = 4 * datarate; + } + } + + if( ceilNumerator < 0 ) + { + ceilNumerator = 0; + } + + // Perform integral ceil() + int32_t intermediate = + ( ( ceilNumerator + ceilDenominator - 1 ) / ceilDenominator ) * crDenom + preambleLen + 12; + + if( datarate <= 6 ) + { + intermediate += 2; + } + + return ( uint32_t )( ( 4 * intermediate + 1 ) * ( 1 << ( datarate - 2 ) ) ); +} + +uint32_t RadioTimeOnAir( RadioModems_t modem, uint32_t bandwidth, + uint32_t datarate, uint8_t coderate, + uint16_t preambleLen, bool fixLen, uint8_t payloadLen, + bool crcOn ) +{ + uint32_t numerator = 0; + uint32_t denominator = 1; + + switch( modem ) + { + case MODEM_FSK: + { + numerator = 1000U * RadioGetGfskTimeOnAirNumerator( datarate, coderate, + preambleLen, fixLen, + payloadLen, crcOn ); + denominator = datarate; + } + break; + case MODEM_LORA: + { + numerator = 1000U * RadioGetLoRaTimeOnAirNumerator( bandwidth, datarate, + coderate, preambleLen, + fixLen, payloadLen, crcOn ); + denominator = RadioGetLoRaBandwidthInHz( Bandwidths[bandwidth] ); + } + break; + } + // Perform integral ceil() + return ( numerator + denominator - 1 ) / denominator; +} + +void RadioSend( uint8_t *buffer, uint8_t size ) +{ + SX126xSetDioIrqParams( IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT, + IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT, + IRQ_RADIO_NONE, + IRQ_RADIO_NONE ); + + if( SX126xGetPacketType( ) == PACKET_TYPE_LORA ) + { + SX126x.PacketParams.Params.LoRa.PayloadLength = size; + } + else + { + SX126x.PacketParams.Params.Gfsk.PayloadLength = size; + } + SX126xSetPacketParams( &SX126x.PacketParams ); + + SX126xSendPayload( buffer, size, 0 ); + TimerSetValue( &TxTimeoutTimer, TxTimeout ); + TimerStart( &TxTimeoutTimer ); +} + +void RadioSleep( void ) +{ + SleepParams_t params = { 0 }; + + params.Fields.WarmStart = 1; + SX126xSetSleep( params ); + + DelayMs( 2 ); +} + +void RadioStandby( void ) +{ + SX126xSetStandby( STDBY_RC ); +} + +void RadioRx( uint32_t timeout ) +{ + SX126xSetDioIrqParams( IRQ_RADIO_ALL, //IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT, + IRQ_RADIO_ALL, //IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT, + IRQ_RADIO_NONE, + IRQ_RADIO_NONE ); + + if( timeout != 0 ) + { + TimerSetValue( &RxTimeoutTimer, timeout ); + TimerStart( &RxTimeoutTimer ); + } + + if( RxContinuous == true ) + { + SX126xSetRx( 0xFFFFFF ); // Rx Continuous + } + else + { + SX126xSetRx( RxTimeout << 6 ); + } +} + +void RadioRxBoosted( uint32_t timeout ) +{ + SX126xSetDioIrqParams( IRQ_RADIO_ALL, //IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT, + IRQ_RADIO_ALL, //IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT, + IRQ_RADIO_NONE, + IRQ_RADIO_NONE ); + + if( timeout != 0 ) + { + TimerSetValue( &RxTimeoutTimer, timeout ); + TimerStart( &RxTimeoutTimer ); + } + + if( RxContinuous == true ) + { + SX126xSetRxBoosted( 0xFFFFFF ); // Rx Continuous + } + else + { + SX126xSetRxBoosted( RxTimeout << 6 ); + } +} + +void RadioSetRxDutyCycle( uint32_t rxTime, uint32_t sleepTime ) +{ + SX126xSetRxDutyCycle( rxTime, sleepTime ); +} + +void RadioAddRegisterToRetentionList( uint16_t registerAddress ) +{ + uint8_t buffer[9]; + + // Read the address and registers already added to the list + SX126xReadRegisters( REG_RETENTION_LIST_BASE_ADDRESS, buffer, 9 ); + + const uint8_t nbOfRegisters = buffer[0]; + uint8_t* registerList = &buffer[1]; + + // Check if the register given as parameter is already added to the list + for( uint8_t i = 0; i < nbOfRegisters; i++ ) + { + if( registerAddress == ( ( uint16_t ) registerList[2 * i] << 8 ) + registerList[2 * i + 1] ) + { + return; + } + } + + if( nbOfRegisters < MAX_NB_REG_IN_RETENTION ) + { + buffer[0] += 1; + registerList[2 * nbOfRegisters] = ( uint8_t )( registerAddress >> 8 ); + registerList[2 * nbOfRegisters + 1] = ( uint8_t )( registerAddress >> 0 ); + + // Update radio with modified list + SX126xWriteRegisters( REG_RETENTION_LIST_BASE_ADDRESS, buffer, 9 ); + } +} + +void RadioStartCad( void ) +{ + SX126xSetDioIrqParams( IRQ_CAD_DONE | IRQ_CAD_ACTIVITY_DETECTED, IRQ_CAD_DONE | IRQ_CAD_ACTIVITY_DETECTED, IRQ_RADIO_NONE, IRQ_RADIO_NONE ); + SX126xSetCad( ); +} + +void RadioSetTxContinuousWave( uint32_t freq, int8_t power, uint16_t time ) +{ + uint32_t timeout = ( uint32_t )time * 1000; + + SX126xSetRfFrequency( freq ); + SX126xSetRfTxPower( power ); + SX126xSetTxContinuousWave( ); + + TimerSetValue( &TxTimeoutTimer, timeout ); + TimerStart( &TxTimeoutTimer ); +} + +int16_t RadioRssi( RadioModems_t modem ) +{ + return SX126xGetRssiInst( ); +} + +void RadioWrite( uint32_t addr, uint8_t data ) +{ + SX126xWriteRegister( addr, data ); +} + +uint8_t RadioRead( uint32_t addr ) +{ + return SX126xReadRegister( addr ); +} + +void RadioWriteBuffer( uint32_t addr, uint8_t *buffer, uint8_t size ) +{ + SX126xWriteRegisters( addr, buffer, size ); +} + +void RadioReadBuffer( uint32_t addr, uint8_t *buffer, uint8_t size ) +{ + SX126xReadRegisters( addr, buffer, size ); +} + +void RadioSetMaxPayloadLength( RadioModems_t modem, uint8_t max ) +{ + if( modem == MODEM_LORA ) + { + SX126x.PacketParams.Params.LoRa.PayloadLength = MaxPayloadLength = max; + SX126xSetPacketParams( &SX126x.PacketParams ); + } + else + { + if( SX126x.PacketParams.Params.Gfsk.HeaderType == RADIO_PACKET_VARIABLE_LENGTH ) + { + SX126x.PacketParams.Params.Gfsk.PayloadLength = MaxPayloadLength = max; + SX126xSetPacketParams( &SX126x.PacketParams ); + } + } +} + +void RadioSetPublicNetwork( bool enable ) +{ + RadioPublicNetwork.Current = RadioPublicNetwork.Previous = enable; + + RadioSetModem( MODEM_LORA ); + if( enable == true ) + { + // Change LoRa modem SyncWord + SX126xWriteRegister( REG_LR_SYNCWORD, ( LORA_MAC_PUBLIC_SYNCWORD >> 8 ) & 0xFF ); + SX126xWriteRegister( REG_LR_SYNCWORD + 1, LORA_MAC_PUBLIC_SYNCWORD & 0xFF ); + } + else + { + // Change LoRa modem SyncWord + SX126xWriteRegister( REG_LR_SYNCWORD, ( LORA_MAC_PRIVATE_SYNCWORD >> 8 ) & 0xFF ); + SX126xWriteRegister( REG_LR_SYNCWORD + 1, LORA_MAC_PRIVATE_SYNCWORD & 0xFF ); + } +} + +uint32_t RadioGetWakeupTime( void ) +{ + return SX126xGetBoardTcxoWakeupTime( ) + RADIO_WAKEUP_TIME; +} + +void RadioOnTxTimeoutIrq( void* context ) +{ + if( ( RadioEvents != NULL ) && ( RadioEvents->TxTimeout != NULL ) ) + { + RadioEvents->TxTimeout( ); + } +} + +void RadioOnRxTimeoutIrq( void* context ) +{ + if( ( RadioEvents != NULL ) && ( RadioEvents->RxTimeout != NULL ) ) + { + RadioEvents->RxTimeout( ); + } +} + +void RadioOnDioIrq( void* context ) +{ + IrqFired = true; +} + +void RadioIrqProcess( void ) +{ + CRITICAL_SECTION_BEGIN( ); + // Clear IRQ flag + const bool isIrqFired = IrqFired; + IrqFired = false; + CRITICAL_SECTION_END( ); + + if( isIrqFired == true ) + { + uint16_t irqRegs = SX126xGetIrqStatus( ); + SX126xClearIrqStatus( irqRegs ); + + // Check if DIO1 pin is High. If it is the case revert IrqFired to true + CRITICAL_SECTION_BEGIN_REPEAT( ); + if( SX126xGetDio1PinState( ) == 1 ) + { + IrqFired = true; + } + CRITICAL_SECTION_END( ); + + if( ( irqRegs & IRQ_TX_DONE ) == IRQ_TX_DONE ) + { + TimerStop( &TxTimeoutTimer ); + //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC + SX126xSetOperatingMode( MODE_STDBY_RC ); + if( ( RadioEvents != NULL ) && ( RadioEvents->TxDone != NULL ) ) + { + RadioEvents->TxDone( ); + } + } + + if( ( irqRegs & IRQ_RX_DONE ) == IRQ_RX_DONE ) + { + TimerStop( &RxTimeoutTimer ); + + if( ( irqRegs & IRQ_CRC_ERROR ) == IRQ_CRC_ERROR ) + { + if( RxContinuous == false ) + { + //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC + SX126xSetOperatingMode( MODE_STDBY_RC ); + } + if( ( RadioEvents != NULL ) && ( RadioEvents->RxError ) ) + { + RadioEvents->RxError( ); + } + } + else + { + uint8_t size; + + if( RxContinuous == false ) + { + //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC + SX126xSetOperatingMode( MODE_STDBY_RC ); + + // WORKAROUND - Implicit Header Mode Timeout Behavior, see DS_SX1261-2_V1.2 datasheet chapter 15.3 + SX126xWriteRegister( REG_RTC_CTRL, 0x00 ); + SX126xWriteRegister( REG_EVT_CLR, SX126xReadRegister( REG_EVT_CLR ) | ( 1 << 1 ) ); + // WORKAROUND END + } + SX126xGetPayload( RadioRxPayload, &size , 255 ); + SX126xGetPacketStatus( &RadioPktStatus ); + if( ( RadioEvents != NULL ) && ( RadioEvents->RxDone != NULL ) ) + { + RadioEvents->RxDone( RadioRxPayload, size, RadioPktStatus.Params.LoRa.RssiPkt, RadioPktStatus.Params.LoRa.SnrPkt ); + } + } + } + + if( ( irqRegs & IRQ_CAD_DONE ) == IRQ_CAD_DONE ) + { + //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC + SX126xSetOperatingMode( MODE_STDBY_RC ); + if( ( RadioEvents != NULL ) && ( RadioEvents->CadDone != NULL ) ) + { + RadioEvents->CadDone( ( ( irqRegs & IRQ_CAD_ACTIVITY_DETECTED ) == IRQ_CAD_ACTIVITY_DETECTED ) ); + } + } + + if( ( irqRegs & IRQ_RX_TX_TIMEOUT ) == IRQ_RX_TX_TIMEOUT ) + { + if( SX126xGetOperatingMode( ) == MODE_TX ) + { + TimerStop( &TxTimeoutTimer ); + //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC + SX126xSetOperatingMode( MODE_STDBY_RC ); + if( ( RadioEvents != NULL ) && ( RadioEvents->TxTimeout != NULL ) ) + { + RadioEvents->TxTimeout( ); + } + } + else if( SX126xGetOperatingMode( ) == MODE_RX ) + { + TimerStop( &RxTimeoutTimer ); + //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC + SX126xSetOperatingMode( MODE_STDBY_RC ); + if( ( RadioEvents != NULL ) && ( RadioEvents->RxTimeout != NULL ) ) + { + RadioEvents->RxTimeout( ); + } + } + } + + if( ( irqRegs & IRQ_PREAMBLE_DETECTED ) == IRQ_PREAMBLE_DETECTED ) + { + //__NOP( ); + } + + if( ( irqRegs & IRQ_SYNCWORD_VALID ) == IRQ_SYNCWORD_VALID ) + { + //__NOP( ); + } + + if( ( irqRegs & IRQ_HEADER_VALID ) == IRQ_HEADER_VALID ) + { + //__NOP( ); + } + + if( ( irqRegs & IRQ_HEADER_ERROR ) == IRQ_HEADER_ERROR ) + { + TimerStop( &RxTimeoutTimer ); + if( RxContinuous == false ) + { + //!< Update operating mode state to a value lower than \ref MODE_STDBY_XOSC + SX126xSetOperatingMode( MODE_STDBY_RC ); + } + if( ( RadioEvents != NULL ) && ( RadioEvents->RxTimeout != NULL ) ) + { + RadioEvents->RxTimeout( ); + } + } + } +} diff --git a/zephcore/patches/zephyr/drivers/gnss/Kconfig.luatos_air530z b/zephcore/patches/zephyr/drivers/gnss/Kconfig.luatos_air530z new file mode 100644 index 0000000..ffc218b --- /dev/null +++ b/zephcore/patches/zephyr/drivers/gnss/Kconfig.luatos_air530z @@ -0,0 +1,43 @@ +# Copyright 2024 Jerónimo Agulló +# SPDX-License-Identifier: Apache-2.0 + +config GNSS_LUATOS_AIR530Z + bool "Luatos Air530z GNSS device" + default y + depends on GNSS + depends on DT_HAS_LUATOS_AIR530Z_ENABLED + depends on GNSS_REFERENCE_FRAME_WGS84 + select MODEM_MODULES + select MODEM_BACKEND_UART + select MODEM_CHAT + select GNSS_PARSE + select GNSS_NMEA0183 + select GNSS_NMEA0183_MATCH + help + Enable Luatos Air530z GNSS driver. + +if GNSS_LUATOS_AIR530Z + +config GNSS_LUATOS_AIR530Z_SATELLITES_COUNT + int "Maximum satellite count" + depends on GNSS_SATELLITES + default 24 + help + Maximum number of satellites that can be decoded from the + GNSS device. This does not affect the number of devices that + the device is actually tracking, just how many of those can + be reported in the satellites callback. + +config GNSS_LUATOS_AIR530Z_EASY + bool "Enable EASY (Embedded Assist System) for faster TTFF" + default y + help + Enable MediaTek EASY (Embedded Assist System) mode via PMTK869 + command. EASY caches predicted ephemeris in the GNSS module's + internal flash, reducing Time-To-First-Fix from 15-45s (cold) + to 1-3s (warm). Sent on every driver init (boot and PM resume). + The setting persists in GNSS flash, so resending is a no-op. + Compatible with L76K/L76KB and other MediaTek-based GNSS chips + that accept PMTK commands alongside PCAS. + +endif diff --git a/zephcore/patches/zephyr/drivers/gnss/gnss_luatos_air530z.c b/zephcore/patches/zephyr/drivers/gnss/gnss_luatos_air530z.c new file mode 100644 index 0000000..7de70ad --- /dev/null +++ b/zephcore/patches/zephyr/drivers/gnss/gnss_luatos_air530z.c @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2024 Jerónimo Agulló + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "gnss_nmea0183.h" +#include "gnss_nmea0183_match.h" +#include "gnss_parse.h" + +#include +LOG_MODULE_REGISTER(luatos_air530z, CONFIG_GNSS_LOG_LEVEL); + +#define DT_DRV_COMPAT luatos_air530z + +#define UART_RECV_BUF_SZ 128 +#define UART_TRANS_BUF_SZ 64 + +#define CHAT_RECV_BUF_SZ 256 +#define CHAT_ARGV_SZ 32 + +MODEM_CHAT_SCRIPT_CMDS_DEFINE(init_script_cmds, +#if CONFIG_GNSS_SATELLITES + /* receive only GGA, RMC and GSV NMEA messages */ + MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS03,1,0,0,1,1,0,0,0,0,0,0,0,0*1F", 10), +#else + /* receive only GGA and RMC NMEA messages */ + MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0*1E", 10), +#endif +#if IS_ENABLED(CONFIG_GNSS_LUATOS_AIR530Z_EASY) + /* Enable EASY (Embedded Assist System) — caches predicted ephemeris + * in GNSS internal flash for 1-3s warm start instead of 15-45s cold. + * PMTK869,1,1 = Set EASY, Enable. Persists across power cycles. + * L76K/L76KB accept PMTK alongside PCAS (both MediaTek MT33xx). */ + MODEM_CHAT_SCRIPT_CMD_RESP_NONE("$PMTK869,1,1*36", 10), +#endif +); + +MODEM_CHAT_SCRIPT_NO_ABORT_DEFINE(init_script, init_script_cmds, NULL, 5); + +struct gnss_luatos_air530z_config { + const struct device *uart; + const struct gpio_dt_spec on_off_gpio; + const int uart_baudrate; +}; + +struct gnss_luatos_air530z_data { + struct gnss_nmea0183_match_data match_data; +#if CONFIG_GNSS_SATELLITES + struct gnss_satellite satellites[CONFIG_GNSS_LUATOS_AIR530Z_SATELLITES_COUNT]; +#endif + + /* UART backend */ + struct modem_pipe *uart_pipe; + struct modem_backend_uart uart_backend; + uint8_t uart_backend_receive_buf[UART_RECV_BUF_SZ]; + uint8_t uart_backend_transmit_buf[UART_TRANS_BUF_SZ]; + + /* Modem chat */ + struct modem_chat chat; + uint8_t chat_receive_buf[CHAT_RECV_BUF_SZ]; + uint8_t chat_delimiter[2]; + uint8_t *chat_argv[CHAT_ARGV_SZ]; + + /* Dynamic chat script */ + uint8_t dynamic_separators_buf[2]; + uint8_t dynamic_request_buf[32]; + struct modem_chat_script_chat dynamic_script_chat; + struct modem_chat_script dynamic_script; + + struct k_sem lock; +}; + +MODEM_CHAT_MATCHES_DEFINE(unsol_matches, + MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback), + MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback), +#if CONFIG_GNSS_SATELLITES + MODEM_CHAT_MATCH_WILDCARD("$??GSV,", ",*", gnss_nmea0183_match_gsv_callback), +#endif +); + +static void luatos_air530z_lock(const struct device *dev) +{ + struct gnss_luatos_air530z_data *data = dev->data; + + (void)k_sem_take(&data->lock, K_FOREVER); +} + +static void luatos_air530z_unlock(const struct device *dev) +{ + struct gnss_luatos_air530z_data *data = dev->data; + + k_sem_give(&data->lock); +} + +static int gnss_luatos_air530z_init_nmea0183_match(const struct device *dev) +{ + struct gnss_luatos_air530z_data *data = dev->data; + + const struct gnss_nmea0183_match_config match_config = { + .gnss = dev, +#if CONFIG_GNSS_SATELLITES + .satellites = data->satellites, + .satellites_size = ARRAY_SIZE(data->satellites), +#endif + }; + + return gnss_nmea0183_match_init(&data->match_data, &match_config); +} + +static void gnss_luatos_air530z_init_pipe(const struct device *dev) +{ + const struct gnss_luatos_air530z_config *config = dev->config; + struct gnss_luatos_air530z_data *data = dev->data; + + const struct modem_backend_uart_config uart_backend_config = { + .uart = config->uart, + .receive_buf = data->uart_backend_receive_buf, + .receive_buf_size = sizeof(data->uart_backend_receive_buf), + .transmit_buf = data->uart_backend_transmit_buf, + .transmit_buf_size = ARRAY_SIZE(data->uart_backend_transmit_buf), + }; + + data->uart_pipe = modem_backend_uart_init(&data->uart_backend, &uart_backend_config); +} + +static int gnss_luatos_air530z_init_chat(const struct device *dev) +{ + struct gnss_luatos_air530z_data *data = dev->data; + + const struct modem_chat_config chat_config = { + .user_data = data, + .receive_buf = data->chat_receive_buf, + .receive_buf_size = sizeof(data->chat_receive_buf), + .delimiter = data->chat_delimiter, + .delimiter_size = ARRAY_SIZE(data->chat_delimiter), + .filter = NULL, + .filter_size = 0, + .argv = data->chat_argv, + .argv_size = ARRAY_SIZE(data->chat_argv), + .unsol_matches = unsol_matches, + .unsol_matches_size = ARRAY_SIZE(unsol_matches), + }; + + return modem_chat_init(&data->chat, &chat_config); +} + +static void luatos_air530z_init_dynamic_script(const struct device *dev) +{ + struct gnss_luatos_air530z_data *data = dev->data; + + /* Air530z doesn't respond to commands. Thus, response_matches_size = 0; */ + data->dynamic_script_chat.request = data->dynamic_request_buf; + data->dynamic_script_chat.response_matches = NULL; + data->dynamic_script_chat.response_matches_size = 0; + data->dynamic_script_chat.timeout = 0; + + data->dynamic_script.name = "PCAS"; + data->dynamic_script.script_chats = &data->dynamic_script_chat; + data->dynamic_script.script_chats_size = 1; + data->dynamic_script.abort_matches = NULL; + data->dynamic_script.abort_matches_size = 0; + data->dynamic_script.callback = NULL; + data->dynamic_script.timeout = 5; +} + +static int gnss_luatos_air530z_init(const struct device *dev) +{ + struct gnss_luatos_air530z_data *data = dev->data; + const struct gnss_luatos_air530z_config *config = dev->config; + int ret; + + k_sem_init(&data->lock, 1, 1); + + ret = gnss_luatos_air530z_init_nmea0183_match(dev); + if (ret < 0) { + return ret; + } + + gnss_luatos_air530z_init_pipe(dev); + + ret = gnss_luatos_air530z_init_chat(dev); + if (ret < 0) { + return ret; + } + + luatos_air530z_init_dynamic_script(dev); + + ret = modem_pipe_open(data->uart_pipe, K_SECONDS(10)); + if (ret < 0) { + return ret; + } + + ret = modem_chat_attach(&data->chat, data->uart_pipe); + if (ret < 0) { + modem_pipe_close(data->uart_pipe, K_SECONDS(10)); + return ret; + } + + ret = modem_chat_run_script(&data->chat, &init_script); + if (ret < 0) { + LOG_ERR("Failed to run init_script"); + modem_pipe_close(data->uart_pipe, K_SECONDS(10)); + return ret; + } + + /* setup on-off gpio for power management */ + if (!gpio_is_ready_dt(&config->on_off_gpio)) { + LOG_ERR("on-off GPIO device not ready"); + return -ENODEV; + } + + gpio_pin_configure_dt(&config->on_off_gpio, GPIO_OUTPUT_HIGH); + + return 0; +} + +/* PM intentionally removed — the L76K ignores $PMTK161,0 (AT6558-based, + * not genuine MediaTek). GPS standby uses GPIO FORCE_ON pin instead. + * Also, CONFIG_PM_DEVICE_SYSTEM_MANAGED can auto-suspend devices during + * idle, calling modem_chat_run_script() from an unexpected context and + * potentially deadlocking the system. */ + +static int luatos_air530z_set_fix_rate(const struct device *dev, uint32_t fix_interval_ms) +{ + struct gnss_luatos_air530z_data *data = dev->data; + int ret; + + if (fix_interval_ms < 100 || fix_interval_ms > 1000) { + return -EINVAL; + } + + luatos_air530z_lock(dev); + + ret = gnss_nmea0183_snprintk(data->dynamic_request_buf, sizeof(data->dynamic_request_buf), + "PCAS02,%u", fix_interval_ms); + if (ret < 0) { + goto unlock_return; + } + + data->dynamic_script_chat.request_size = ret; + + ret = modem_chat_run_script(&data->chat, &data->dynamic_script); + if (ret < 0) { + goto unlock_return; + } + +unlock_return: + luatos_air530z_unlock(dev); + return ret; +} + +static int luatos_air530z_set_enabled_systems(const struct device *dev, gnss_systems_t systems) +{ + struct gnss_luatos_air530z_data *data = dev->data; + gnss_systems_t supported_systems; + uint8_t encoded_systems = 0; + int ret; + + supported_systems = (GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | GNSS_SYSTEM_BEIDOU); + + if ((~supported_systems) & systems) { + return -EINVAL; + } + + luatos_air530z_lock(dev); + + WRITE_BIT(encoded_systems, 0, systems & GNSS_SYSTEM_GPS); + WRITE_BIT(encoded_systems, 1, systems & GNSS_SYSTEM_GLONASS); + WRITE_BIT(encoded_systems, 2, systems & GNSS_SYSTEM_BEIDOU); + + ret = gnss_nmea0183_snprintk(data->dynamic_request_buf, sizeof(data->dynamic_request_buf), + "PCAS04,%u", encoded_systems); + if (ret < 0) { + goto unlock_return; + } + + data->dynamic_script_chat.request_size = ret; + + ret = modem_chat_run_script(&data->chat, &data->dynamic_script); + if (ret < 0) { + goto unlock_return; + } + +unlock_return: + luatos_air530z_unlock(dev); + return ret; + +} + +static int luatos_air530z_get_supported_systems(const struct device *dev, gnss_systems_t *systems) +{ + *systems = (GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | GNSS_SYSTEM_BEIDOU); + return 0; +} + +static DEVICE_API(gnss, gnss_api) = { + .set_fix_rate = luatos_air530z_set_fix_rate, + .set_enabled_systems = luatos_air530z_set_enabled_systems, + .get_supported_systems = luatos_air530z_get_supported_systems, +}; + +#define LUATOS_AIR530Z(inst) \ + static const struct gnss_luatos_air530z_config gnss_luatos_air530z_cfg_##inst = { \ + .uart = DEVICE_DT_GET(DT_INST_BUS(inst)), \ + .on_off_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, on_off_gpios, { 0 }), \ + }; \ + \ + static struct gnss_luatos_air530z_data gnss_luatos_air530z_data_##inst = { \ + .chat_delimiter = {'\r', '\n'}, \ + .dynamic_separators_buf = {',', '*'}, \ + }; \ + \ + DEVICE_DT_INST_DEFINE(inst, gnss_luatos_air530z_init, \ + NULL, \ + &gnss_luatos_air530z_data_##inst, \ + &gnss_luatos_air530z_cfg_##inst, \ + POST_KERNEL, CONFIG_GNSS_INIT_PRIORITY, &gnss_api); + +DT_INST_FOREACH_STATUS_OKAY(LUATOS_AIR530Z) diff --git a/zephcore/patches/zephyr/drivers/lora/CMakeLists.txt b/zephcore/patches/zephyr/drivers/lora/CMakeLists.txt new file mode 100644 index 0000000..cda2f6d --- /dev/null +++ b/zephcore/patches/zephyr/drivers/lora/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +# ZephCore patch: adds lr11xx driver subdirectory. + +zephyr_sources_ifdef(CONFIG_LORA_SHELL shell.c) + +add_subdirectory_ifdef(CONFIG_LORA_LR11XX lr11xx) + +# zephyr-keep-sorted-start +add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE loramac-node) +add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_LORA_BASICS_MODEM lora-basics-modem) +add_subdirectory_ifdef(CONFIG_LORA_MODULE_BACKEND_NATIVE native) +# zephyr-keep-sorted-stop diff --git a/zephcore/patches/zephyr/drivers/lora/Kconfig b/zephcore/patches/zephyr/drivers/lora/Kconfig new file mode 100644 index 0000000..0ebee42 --- /dev/null +++ b/zephcore/patches/zephyr/drivers/lora/Kconfig @@ -0,0 +1,68 @@ +# +# Copyright (c) 2019 Manivannan Sadhasivam +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Top-level configuration file for LORA drivers. +# ZephCore patch: adds lr11xx driver Kconfig. + +menuconfig LORA + bool "LoRa drivers" + select POLL + help + Include LoRa drivers in the system configuration. + +if LORA + +choice LORA_MODULE_BACKEND + prompt "Low-level LoRa modem integration to use" + default LORA_MODULE_BACKEND_LORA_BASICS_MODEM if DT_HAS_SEMTECH_SX1268_ENABLED \ + || DT_HAS_SEMTECH_LLCC68_ENABLED || DT_HAS_SEMTECH_SX1278_ENABLED + +config LORA_MODULE_BACKEND_LORAMAC_NODE + bool "loramac-node backend" + depends on ZEPHYR_LORAMAC_NODE_MODULE + +config LORA_MODULE_BACKEND_LORA_BASICS_MODEM + bool "LoRa Basic modem backend" + depends on ZEPHYR_LORA_BASICS_MODEM_MODULE + depends on DT_HAS_SEMTECH_SX1262_ENABLED || DT_HAS_SEMTECH_SX1261_ENABLED \ + || DT_HAS_SEMTECH_SX1272_ENABLED || DT_HAS_SEMTECH_SX1276_ENABLED \ + || DT_HAS_SEMTECH_SX1268_ENABLED || DT_HAS_SEMTECH_LLCC68_ENABLED \ + || DT_HAS_SEMTECH_SX1278_ENABLED + select USE_LORA_BASICS_MODEM_DRIVERS + help + LoRa API support using the LoRa Basics Modem module. + +config LORA_MODULE_BACKEND_NATIVE + bool "Native Zephyr backend" + help + Use Zephyr's built-in LoRa drivers without external modules. + Currently supports SX1261/SX1262 transceivers. + +endchoice + +module = LORA +module-str = lora +source "subsys/logging/Kconfig.template.log_config" + +config LORA_SHELL + bool "LoRa Shell" + depends on SHELL + help + Enable LoRa Shell for testing. + +config LORA_INIT_PRIORITY + int "LoRa initialization priority" + default 90 + help + System initialization priority for LoRa drivers. + +rsource "Kconfig.sx12xx" +rsource "Kconfig.rylrxxx" +rsource "lr11xx/Kconfig" +rsource "lora-basics-modem/Kconfig" +rsource "native/Kconfig" + +endif # LORA diff --git a/zephcore/patches/zephyr/drivers/lora/loramac-node/sx12xx_common.c b/zephcore/patches/zephyr/drivers/lora/loramac-node/sx12xx_common.c new file mode 100644 index 0000000..ad183cd --- /dev/null +++ b/zephcore/patches/zephyr/drivers/lora/loramac-node/sx12xx_common.c @@ -0,0 +1,462 @@ +/* + * Copyright (c) 2019 Manivannan Sadhasivam + * Copyright (c) 2020 Grinn + * + * SPDX-License-Identifier: Apache-2.0 + * + * ZEPHCORE PATCH: Modified sx12xx_ev_rx_error() to notify async callback + * with NULL data on RX errors (CRC/header). This allows error counting. + */ + +#include +#include +#include +#include +#include + +/* LoRaMac-node specific includes */ +#include + +#include "sx12xx_common.h" + +#define STATE_FREE 0 +#define STATE_BUSY 1 +#define STATE_CLEANUP 2 + +LOG_MODULE_REGISTER(sx12xx_common, CONFIG_LORA_LOG_LEVEL); + +struct sx12xx_rx_params { + uint8_t *buf; + uint8_t *size; + int16_t *rssi; + int8_t *snr; +}; + +static struct sx12xx_data { + const struct device *dev; + struct k_poll_signal *operation_done; + lora_recv_cb async_rx_cb; + void *async_user_data; + RadioEvents_t events; + struct lora_modem_config tx_cfg; + atomic_t modem_usage; + struct sx12xx_rx_params rx_params; +} dev_data; + +int __sx12xx_configure_pin(const struct gpio_dt_spec *gpio, gpio_flags_t flags) +{ + int err; + + if (!device_is_ready(gpio->port)) { + LOG_ERR("GPIO device not ready %s", gpio->port->name); + return -ENODEV; + } + + err = gpio_pin_configure_dt(gpio, flags); + if (err) { + LOG_ERR("Cannot configure gpio %s %d: %d", gpio->port->name, + gpio->pin, err); + return err; + } + + return 0; +} + +/** + * @brief Attempt to acquire the modem for operations + * + * @param data common sx12xx data struct + * + * @retval true if modem was acquired + * @retval false otherwise + */ +static inline bool modem_acquire(struct sx12xx_data *data) +{ + return atomic_cas(&data->modem_usage, STATE_FREE, STATE_BUSY); +} + +/** + * @brief Safely release the modem from any context + * + * This function can be called from any context and guarantees that the + * release operations will only be run once. + * + * @param data common sx12xx data struct + * + * @retval true if modem was released by this function + * @retval false otherwise + */ +static bool modem_release(struct sx12xx_data *data) +{ + /* Increment atomic so both acquire and release will fail */ + if (!atomic_cas(&data->modem_usage, STATE_BUSY, STATE_CLEANUP)) { + return false; + } + /* Put radio back into sleep mode */ + Radio.Sleep(); + /* Completely release modem */ + data->operation_done = NULL; + atomic_clear(&data->modem_usage); + return true; +} + +static void sx12xx_ev_rx_done(uint8_t *payload, uint16_t size, int16_t rssi, + int8_t snr) +{ + struct k_poll_signal *sig = dev_data.operation_done; + + /* Receiving in asynchronous mode */ + if (dev_data.async_rx_cb) { + /* Start receiving again */ + Radio.Rx(0); + /* Run the callback */ + dev_data.async_rx_cb(dev_data.dev, payload, size, rssi, snr, + dev_data.async_user_data); + /* Don't run the synchronous code */ + return; + } + + /* Manually release the modem instead of just calling modem_release + * as we need to perform cleanup operations while still ensuring + * others can't use the modem. + */ + if (!atomic_cas(&dev_data.modem_usage, STATE_BUSY, STATE_CLEANUP)) { + return; + } + /* We can make two observations here: + * 1. lora_recv hasn't already exited due to a timeout. + * (modem_release would have been successfully called) + * 2. If the k_poll in lora_recv times out before we raise the signal, + * but while this code is running, it will block on the + * signal again. + * This lets us guarantee that the operation_done signal and pointers + * in rx_params are always valid in this function. + */ + + /* Store actual size */ + if (size < *dev_data.rx_params.size) { + *dev_data.rx_params.size = size; + } + /* Copy received data to output buffer */ + memcpy(dev_data.rx_params.buf, payload, + *dev_data.rx_params.size); + /* Output RSSI and SNR */ + if (dev_data.rx_params.rssi) { + *dev_data.rx_params.rssi = rssi; + } + if (dev_data.rx_params.snr) { + *dev_data.rx_params.snr = snr; + } + /* Put radio back into sleep mode */ + Radio.Sleep(); + /* Completely release modem */ + dev_data.operation_done = NULL; + atomic_clear(&dev_data.modem_usage); + /* Notify caller RX is complete */ + k_poll_signal_raise(sig, 0); +} + +static void sx12xx_ev_tx_done(void) +{ + struct k_poll_signal *sig = dev_data.operation_done; + + if (modem_release(&dev_data)) { + /* Raise signal if provided */ + if (sig) { + k_poll_signal_raise(sig, 0); + } + } +} + +static void sx12xx_ev_tx_timed_out(void) +{ + /* Just release the modem */ + modem_release(&dev_data); +} + +static void sx12xx_ev_rx_error(void) +{ + struct k_poll_signal *sig = dev_data.operation_done; + + /* Receiving in asynchronous mode */ + if (dev_data.async_rx_cb) { + /* Start receiving again */ + Radio.Rx(0); + /* + * ZEPHCORE PATCH: Notify callback with NULL data to indicate + * RX error (CRC mismatch, header error). This allows the + * application to count receive errors for diagnostics. + */ + dev_data.async_rx_cb(dev_data.dev, NULL, 0, 0, 0, + dev_data.async_user_data); + /* Don't run the synchronous code */ + return; + } + + /* Finish synchronous receive with error */ + if (modem_release(&dev_data)) { + /* Raise signal if provided */ + if (sig) { + k_poll_signal_raise(sig, -EIO); + } + } +} + +/** + * @brief Convert Zephyr bandwidth enum to loramac-node bandwidth index + * + * The loramac-node library expects bandwidth as an index into its internal + * Bandwidths[] array: {BW_007, BW_010, BW_015, BW_020, BW_031, + * BW_041, BW_062, BW_125, BW_250, BW_500} + */ +static int sx12xx_get_bandwidth_idx(enum lora_signal_bandwidth bandwidth, + uint32_t *bw_idx) +{ + switch (bandwidth) { + case BW_7_KHZ: *bw_idx = 0; break; + case BW_10_KHZ: *bw_idx = 1; break; + case BW_15_KHZ: *bw_idx = 2; break; + case BW_20_KHZ: *bw_idx = 3; break; + case BW_31_KHZ: *bw_idx = 4; break; + case BW_41_KHZ: *bw_idx = 5; break; + case BW_62_KHZ: *bw_idx = 6; break; + case BW_125_KHZ: *bw_idx = 7; break; + case BW_250_KHZ: *bw_idx = 8; break; + case BW_500_KHZ: *bw_idx = 9; break; + default: + return -EINVAL; + } + return 0; +} + +uint32_t sx12xx_airtime(const struct device *dev, uint32_t data_len) +{ + uint32_t bw_idx; + + if (sx12xx_get_bandwidth_idx(dev_data.tx_cfg.bandwidth, &bw_idx) < 0) { + bw_idx = 0; + } + + return Radio.TimeOnAir(MODEM_LORA, + bw_idx, + dev_data.tx_cfg.datarate, + dev_data.tx_cfg.coding_rate, + dev_data.tx_cfg.preamble_len, + 0, data_len, !dev_data.tx_cfg.packet_crc_disable); +} + +int sx12xx_lora_send(const struct device *dev, uint8_t *data, + uint32_t data_len) +{ + struct k_poll_signal done = K_POLL_SIGNAL_INITIALIZER(done); + struct k_poll_event evt = K_POLL_EVENT_INITIALIZER( + K_POLL_TYPE_SIGNAL, + K_POLL_MODE_NOTIFY_ONLY, + &done); + uint32_t air_time; + int ret; + + /* Validate that we have a TX configuration */ + if (!dev_data.tx_cfg.frequency) { + return -EINVAL; + } + + ret = sx12xx_lora_send_async(dev, data, data_len, &done); + if (ret < 0) { + return ret; + } + + /* Calculate expected airtime of the packet */ + air_time = sx12xx_airtime(dev, data_len); + LOG_DBG("Expected air time of %u bytes = %u ms", data_len, air_time); + + /* Wait for the packet to finish transmitting. + * Use twice the tx duration to ensure that we are actually detecting + * a failed transmission, and not some minor timing variation between + * modem and driver. + */ + ret = k_poll(&evt, 1, K_MSEC(2 * air_time)); + if (ret < 0) { + LOG_ERR("Packet transmission failed!"); + if (!modem_release(&dev_data)) { + /* TX done interrupt is currently running */ + k_poll(&evt, 1, K_FOREVER); + } + } + return ret; +} + +int sx12xx_lora_send_async(const struct device *dev, uint8_t *data, + uint32_t data_len, struct k_poll_signal *async) +{ + /* Ensure available, freed by sx12xx_ev_tx_done */ + if (!modem_acquire(&dev_data)) { + return -EBUSY; + } + + /* Store signal */ + dev_data.operation_done = async; + + Radio.SetMaxPayloadLength(MODEM_LORA, data_len); + + Radio.Send(data, data_len); + + return 0; +} + +int sx12xx_lora_recv(const struct device *dev, uint8_t *data, uint8_t size, + k_timeout_t timeout, int16_t *rssi, int8_t *snr) +{ + struct k_poll_signal done = K_POLL_SIGNAL_INITIALIZER(done); + struct k_poll_event evt = K_POLL_EVENT_INITIALIZER( + K_POLL_TYPE_SIGNAL, + K_POLL_MODE_NOTIFY_ONLY, + &done); + int ret; + + /* Ensure available, decremented by sx12xx_ev_rx_done or on timeout */ + if (!modem_acquire(&dev_data)) { + return -EBUSY; + } + + dev_data.async_rx_cb = NULL; + /* Store operation signal */ + dev_data.operation_done = &done; + /* Set data output location */ + dev_data.rx_params.buf = data; + dev_data.rx_params.size = &size; + dev_data.rx_params.rssi = rssi; + dev_data.rx_params.snr = snr; + + Radio.SetMaxPayloadLength(MODEM_LORA, 255); + Radio.Rx(0); + + ret = k_poll(&evt, 1, timeout); + if (ret < 0) { + if (!modem_release(&dev_data)) { + /* Releasing the modem failed, which means that + * the RX callback is currently running. Wait until + * the RX callback finishes and we get our packet. + */ + k_poll(&evt, 1, K_FOREVER); + + /* We did receive a packet */ + return size; + } + LOG_INF("Receive timeout"); + return ret; + } + + if (done.result < 0) { + LOG_ERR("Receive error"); + return done.result; + } + + return size; +} + +int sx12xx_lora_recv_async(const struct device *dev, lora_recv_cb cb, void *user_data) +{ + /* Cancel ongoing reception */ + if (cb == NULL) { + if (!modem_release(&dev_data)) { + /* Not receiving or already being stopped */ + return -EINVAL; + } + return 0; + } + + /* Ensure available */ + if (!modem_acquire(&dev_data)) { + return -EBUSY; + } + + /* Store parameters */ + dev_data.async_rx_cb = cb; + dev_data.async_user_data = user_data; + + /* Start reception */ + Radio.SetMaxPayloadLength(MODEM_LORA, 255); + Radio.Rx(0); + + return 0; +} + +int sx12xx_lora_config(const struct device *dev, + struct lora_modem_config *config) +{ + bool crc = !config->packet_crc_disable; + uint32_t bw_idx; + int ret; + + ret = sx12xx_get_bandwidth_idx(config->bandwidth, &bw_idx); + if (ret < 0) { + LOG_ERR("Unsupported bandwidth: %d", config->bandwidth); + return ret; + } + + /* Ensure available, decremented after configuration */ + if (!modem_acquire(&dev_data)) { + return -EBUSY; + } + + Radio.SetChannel(config->frequency); + + if (config->tx) { + /* Store TX config locally for airtime calculations */ + memcpy(&dev_data.tx_cfg, config, sizeof(dev_data.tx_cfg)); + /* Configure radio driver */ + Radio.SetTxConfig(MODEM_LORA, config->tx_power, 0, + bw_idx, config->datarate, + config->coding_rate, config->preamble_len, + false, crc, 0, 0, config->iq_inverted, 4000); + } else { + /* TODO: Get symbol timeout value from config parameters */ + Radio.SetRxConfig(MODEM_LORA, bw_idx, + config->datarate, config->coding_rate, + 0, config->preamble_len, 10, false, 0, + crc, false, 0, config->iq_inverted, true); + } + + Radio.SetPublicNetwork(config->public_network); + + modem_release(&dev_data); + return 0; +} + +int sx12xx_lora_test_cw(const struct device *dev, uint32_t frequency, + int8_t tx_power, + uint16_t duration) +{ + /* Ensure available, freed in sx12xx_ev_tx_done */ + if (!modem_acquire(&dev_data)) { + return -EBUSY; + } + + Radio.SetTxContinuousWave(frequency, tx_power, duration); + return 0; +} + +int sx12xx_init(const struct device *dev) +{ + atomic_set(&dev_data.modem_usage, 0); + + dev_data.dev = dev; + dev_data.events.TxDone = sx12xx_ev_tx_done; + dev_data.events.RxDone = sx12xx_ev_rx_done; + dev_data.events.RxError = sx12xx_ev_rx_error; + /* TX timeout event raises at the end of the test CW transmission */ + dev_data.events.TxTimeout = sx12xx_ev_tx_timed_out; + Radio.Init(&dev_data.events); + + /* + * Automatically place the radio into sleep mode upon boot. + * The required `lora_config` call before transmission or reception + * will bring the radio out of sleep mode before it is used. The radio + * is automatically placed back into sleep mode upon TX or RX + * completion. + */ + Radio.Sleep(); + + return 0; +} diff --git a/zephcore/patches/zephyr/drivers/lora/lr11xx/CMakeLists.txt b/zephcore/patches/zephyr/drivers/lora/lr11xx/CMakeLists.txt new file mode 100644 index 0000000..46cf43b --- /dev/null +++ b/zephcore/patches/zephyr/drivers/lora/lr11xx/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (c) 2025 ZephCore +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() + +# Suppress deprecated sync-word warning from Semtech SDK +zephyr_library_compile_definitions(LR11XX_DISABLE_WARNINGS) + +# LR11xx Zephyr LoRa driver +zephyr_library_sources(lr11xx_lora.c) + +# Semtech lr11xx_driver SDK (unchanged, lives in app tree) +# APPLICATION_SOURCE_DIR points to the zephcore/ app directory +set(LR11XX_SDK_DIR ${APPLICATION_SOURCE_DIR}/adapters/radio/lr11xx) +zephyr_library_sources(${LR11XX_SDK_DIR}/lr11xx_hal_zephyr.c) +zephyr_library_sources(${LR11XX_SDK_DIR}/lr11xx_radio.c) +zephyr_library_sources(${LR11XX_SDK_DIR}/lr11xx_radio_timings.c) +zephyr_library_sources(${LR11XX_SDK_DIR}/lr11xx_system.c) +zephyr_library_sources(${LR11XX_SDK_DIR}/lr11xx_regmem.c) + +zephyr_library_include_directories(${LR11XX_SDK_DIR}) + +# Ensure offsets.h is generated before we compile (needed for SPI headers) +zephyr_library_add_dependencies(offsets_h) diff --git a/zephcore/patches/zephyr/drivers/lora/lr11xx/Kconfig b/zephcore/patches/zephyr/drivers/lora/lr11xx/Kconfig new file mode 100644 index 0000000..cd1c727 --- /dev/null +++ b/zephcore/patches/zephyr/drivers/lora/lr11xx/Kconfig @@ -0,0 +1,12 @@ +# Copyright (c) 2025 MeshCore +# SPDX-License-Identifier: Apache-2.0 + +config LORA_LR11XX + bool "Semtech LR11xx LoRa transceiver driver" + default y + depends on DT_HAS_SEMTECH_LR1110_ENABLED + select SPI + select GPIO + help + Enable driver for Semtech LR1110/LR1120/LR1121 LoRa transceivers. + Uses native Semtech lr11xx_driver SDK with Zephyr LoRa driver API. diff --git a/zephcore/patches/zephyr/drivers/lora/lr11xx/lr11xx_lora.c b/zephcore/patches/zephyr/drivers/lora/lr11xx/lr11xx_lora.c new file mode 100644 index 0000000..b67f44f --- /dev/null +++ b/zephcore/patches/zephyr/drivers/lora/lr11xx/lr11xx_lora.c @@ -0,0 +1,873 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * LR11xx Zephyr LoRa driver + * + * Implements the standard Zephyr lora_driver_api using the Semtech lr11xx_driver + * SDK. All SPI access, DIO1 IRQ handling, and radio state management is internal. + */ + +#define DT_DRV_COMPAT semtech_lr1110 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lr11xx_lora.h" +#include "lr11xx_hal_zephyr.h" +#include "lr11xx_radio.h" +#include "lr11xx_radio_types.h" +#include "lr11xx_system.h" +#include "lr11xx_system_types.h" +#include "lr11xx_regmem.h" + +LOG_MODULE_REGISTER(lr11xx_lora, CONFIG_LORA_LOG_LEVEL); + +/* ── Driver data structures ─────────────────────────────────────────── */ + +struct lr11xx_config { + struct spi_dt_spec bus; + struct gpio_dt_spec reset; + struct gpio_dt_spec busy; + struct gpio_dt_spec dio1; + uint16_t tcxo_voltage_mv; + uint32_t tcxo_startup_delay_ms; + bool rx_boosted; + /* RF switch DIO bitmasks */ + uint8_t rfswitch_enable; + uint8_t rfswitch_standby; + uint8_t rfswitch_rx; + uint8_t rfswitch_tx; + uint8_t rfswitch_tx_hp; + uint8_t rfswitch_gnss; + /* PA config */ + uint8_t pa_hp_sel; + uint8_t pa_duty_cycle; +}; + +struct lr11xx_data { + const struct device *dev; + struct lr11xx_hal_context hal_ctx; + struct k_mutex spi_mutex; + + /* Cached modem config from lora_config() */ + struct lora_modem_config modem_cfg; + bool configured; + + /* Async RX state */ + lora_recv_cb async_rx_cb; + void *async_rx_user_data; + + /* Async TX state */ + struct k_poll_signal *tx_signal; + + /* DIO1 work queue */ + struct k_work dio1_work; + + /* Radio state */ + volatile bool tx_active; + volatile bool in_rx_mode; + + /* Extension features (duty cycle, boost) */ + bool rx_duty_cycle_enabled; + bool rx_boost_enabled; + + /* Deferred hardware init — heavy SPI/radio work runs on first config() */ + bool hw_initialized; + + /* RX data buffer — filled in DIO1 handler, passed to callback */ + uint8_t rx_buf[256]; +}; + +/* ── Helpers ────────────────────────────────────────────────────────── */ + +static lr11xx_radio_lora_bw_t bw_enum_to_lr11xx(enum lora_signal_bandwidth bw) +{ + switch (bw) { + case BW_10_KHZ: return LR11XX_RADIO_LORA_BW_10; + case BW_15_KHZ: return LR11XX_RADIO_LORA_BW_15; + case BW_20_KHZ: return LR11XX_RADIO_LORA_BW_20; + case BW_31_KHZ: return LR11XX_RADIO_LORA_BW_31; + case BW_41_KHZ: return LR11XX_RADIO_LORA_BW_41; + case BW_62_KHZ: return LR11XX_RADIO_LORA_BW_62; + case BW_125_KHZ: return LR11XX_RADIO_LORA_BW_125; + case BW_250_KHZ: return LR11XX_RADIO_LORA_BW_250; + case BW_500_KHZ: return LR11XX_RADIO_LORA_BW_500; + default: return LR11XX_RADIO_LORA_BW_125; + } +} + +static lr11xx_radio_lora_cr_t cr_enum_to_lr11xx(enum lora_coding_rate cr) +{ + switch (cr) { + case CR_4_5: return LR11XX_RADIO_LORA_CR_4_5; + case CR_4_6: return LR11XX_RADIO_LORA_CR_4_6; + case CR_4_7: return LR11XX_RADIO_LORA_CR_4_7; + case CR_4_8: return LR11XX_RADIO_LORA_CR_4_8; + default: return LR11XX_RADIO_LORA_CR_4_8; + } +} + +static lr11xx_system_tcxo_supply_voltage_t get_tcxo_voltage(uint16_t mv) +{ + if (mv >= 3300) return LR11XX_SYSTEM_TCXO_CTRL_3_3V; + if (mv >= 3000) return LR11XX_SYSTEM_TCXO_CTRL_3_0V; + if (mv >= 2700) return LR11XX_SYSTEM_TCXO_CTRL_2_7V; + if (mv >= 2400) return LR11XX_SYSTEM_TCXO_CTRL_2_4V; + if (mv >= 2200) return LR11XX_SYSTEM_TCXO_CTRL_2_2V; + if (mv >= 1800) return LR11XX_SYSTEM_TCXO_CTRL_1_8V; + return LR11XX_SYSTEM_TCXO_CTRL_1_6V; +} + +/* Get kHz value from Zephyr BW enum — needed for duty cycle timing */ +static float bw_enum_to_khz(enum lora_signal_bandwidth bw) +{ + switch (bw) { + case BW_7_KHZ: return 7.81f; + case BW_10_KHZ: return 10.42f; + case BW_15_KHZ: return 15.63f; + case BW_20_KHZ: return 20.83f; + case BW_31_KHZ: return 31.25f; + case BW_41_KHZ: return 41.67f; + case BW_62_KHZ: return 62.5f; + case BW_125_KHZ: return 125.0f; + case BW_250_KHZ: return 250.0f; + case BW_500_KHZ: return 500.0f; + default: return 125.0f; + } +} + +/* ── Hardware reset (BUSY stuck recovery) ───────────────────────────── */ + +static void lr11xx_hardware_reset(struct lr11xx_data *data, + const struct lr11xx_config *cfg) +{ + void *ctx = &data->hal_ctx; + + LOG_WRN("LR1110 hardware reset (BUSY stuck recovery)"); + + lr11xx_hal_reset(ctx); + + /* TCXO */ + if (cfg->tcxo_voltage_mv > 0) { + lr11xx_system_set_tcxo_mode(ctx, get_tcxo_voltage(cfg->tcxo_voltage_mv), + 164); + } + + lr11xx_system_set_reg_mode(ctx, LR11XX_SYSTEM_REG_MODE_DCDC); + + /* RF switch */ + lr11xx_system_rfswitch_cfg_t rfsw = { + .enable = cfg->rfswitch_enable, + .standby = cfg->rfswitch_standby, + .rx = cfg->rfswitch_rx, + .tx = cfg->rfswitch_tx, + .tx_hp = cfg->rfswitch_tx_hp, + .tx_hf = 0, + .gnss = cfg->rfswitch_gnss, + .wifi = 0, + }; + lr11xx_system_set_dio_as_rf_switch(ctx, &rfsw); + + lr11xx_system_calibrate(ctx, 0x3F); + + uint16_t freq_mhz = data->modem_cfg.frequency / 1000000; + uint16_t cal_low = (freq_mhz / 4) * 4; + lr11xx_system_calibrate_image_in_mhz(ctx, cal_low, cal_low + 4); + + lr11xx_radio_set_pkt_type(ctx, LR11XX_RADIO_PKT_TYPE_LORA); + + lr11xx_hal_enable_dio1_irq(&data->hal_ctx); + + LOG_WRN("LR1110 recovered from hardware reset"); +} + +/* ── Apply modem configuration ──────────────────────────────────────── */ + +static void lr11xx_apply_modem_config(struct lr11xx_data *data, + const struct lr11xx_config *cfg, + bool tx_mode) +{ + void *ctx = &data->hal_ctx; + struct lora_modem_config *mc = &data->modem_cfg; + + lr11xx_radio_set_rf_freq(ctx, mc->frequency); + + lr11xx_radio_mod_params_lora_t mod = { + .sf = (lr11xx_radio_lora_sf_t)mc->datarate, + .bw = bw_enum_to_lr11xx(mc->bandwidth), + .cr = cr_enum_to_lr11xx(mc->coding_rate), + .ldro = 0, + }; + lr11xx_radio_set_lora_mod_params(ctx, &mod); + + lr11xx_radio_pkt_params_lora_t pkt = { + .preamble_len_in_symb = mc->preamble_len, + .header_type = LR11XX_RADIO_LORA_PKT_EXPLICIT, + .pld_len_in_bytes = 255, + .crc = mc->packet_crc_disable ? LR11XX_RADIO_LORA_CRC_OFF + : LR11XX_RADIO_LORA_CRC_ON, + .iq = mc->iq_inverted ? LR11XX_RADIO_LORA_IQ_INVERTED + : LR11XX_RADIO_LORA_IQ_STANDARD, + }; + lr11xx_radio_set_lora_pkt_params(ctx, &pkt); + + lr11xx_radio_set_lora_sync_word(ctx, mc->public_network ? 0x34 : 0x12); + + if (tx_mode) { + lr11xx_radio_set_tx_params(ctx, mc->tx_power, + LR11XX_RADIO_RAMP_48_US); + lr11xx_radio_pa_cfg_t pa = { + .pa_sel = LR11XX_RADIO_PA_SEL_HP, + .pa_reg_supply = LR11XX_RADIO_PA_REG_SUPPLY_VBAT, + .pa_duty_cycle = cfg->pa_duty_cycle, + .pa_hp_sel = cfg->pa_hp_sel, + }; + lr11xx_radio_set_pa_cfg(ctx, &pa); + } + + lr11xx_system_set_dio_irq_params( + ctx, + LR11XX_SYSTEM_IRQ_RX_DONE | LR11XX_SYSTEM_IRQ_TX_DONE | + LR11XX_SYSTEM_IRQ_TIMEOUT | LR11XX_SYSTEM_IRQ_CRC_ERROR | + LR11XX_SYSTEM_IRQ_HEADER_ERROR, + 0); +} + +/* ── RX duty cycle (RadioLib algorithm) ─────────────────────────────── */ + +static void lr11xx_apply_rx_duty_cycle(struct lr11xx_data *data) +{ + void *ctx = &data->hal_ctx; + struct lora_modem_config *mc = &data->modem_cfg; + + uint8_t sf = (uint8_t)mc->datarate; + float bw_khz = bw_enum_to_khz(mc->bandwidth); + uint16_t preamble_len = mc->preamble_len; + uint16_t min_symbols = (sf >= 7) ? 8 : 12; + + int16_t sleep_symbols = (int16_t)preamble_len - (int16_t)min_symbols; + if (sleep_symbols <= 0) { + LOG_WRN("Preamble too short for duty cycle, using continuous RX"); + data->rx_duty_cycle_enabled = false; + lr11xx_radio_set_rx(ctx, 0xFFFFFF); + return; + } + + uint32_t symbol_us = ((uint32_t)(1 << sf) * 1000) / (uint32_t)bw_khz; + uint32_t sleep_period_us = sleep_symbols * symbol_us; + uint32_t preamble_total_us = (preamble_len + 1) * symbol_us; + int32_t wake_calc1 = ((int32_t)preamble_total_us - + ((int32_t)sleep_period_us - 1000)) / 2; + uint32_t wake_calc2 = (min_symbols + 1) * symbol_us; + uint32_t wake_period_us = (wake_calc1 > 0 && + (uint32_t)wake_calc1 > wake_calc2) + ? (uint32_t)wake_calc1 : wake_calc2; + + /* LR1110 API takes milliseconds */ + uint32_t rx_ms = (wake_period_us + 500) / 1000; + uint32_t sleep_ms = (sleep_period_us + 500) / 1000; + if (rx_ms < 1) rx_ms = 1; + if (sleep_ms < 1) sleep_ms = 1; + + lr11xx_radio_set_rx_duty_cycle(ctx, rx_ms, sleep_ms, + LR11XX_RADIO_RX_DUTY_CYCLE_MODE_RX); + + LOG_INF("RX duty cycle: rx=%ums sleep=%ums (SF%d BW%.0f)", + rx_ms, sleep_ms, sf, (double)bw_khz); +} + +/* ── Start RX (internal) ────────────────────────────────────────────── */ + +static void lr11xx_start_rx(struct lr11xx_data *data, + const struct lr11xx_config *cfg) +{ + void *ctx = &data->hal_ctx; + + /* Standby first — wake from any sleep state */ + data->hal_ctx.radio_is_sleeping = true; + lr11xx_status_t rc = lr11xx_system_set_standby(ctx, + LR11XX_SYSTEM_STANDBY_CFG_RC); + if (rc != LR11XX_STATUS_OK) { + LOG_ERR("standby failed — triggering HW reset"); + lr11xx_hardware_reset(data, cfg); + } + + lr11xx_system_clear_irq_status(ctx, LR11XX_SYSTEM_IRQ_ALL_MASK); + + /* Apply modem config for RX */ + lr11xx_apply_modem_config(data, cfg, false); + + /* Start RX — continuous or duty cycle */ + if (data->rx_duty_cycle_enabled) { + lr11xx_apply_rx_duty_cycle(data); + } else { + lr11xx_radio_set_rx(ctx, 0xFFFFFF); + } + + /* RX boost */ + if (data->rx_boost_enabled) { + lr11xx_radio_cfg_rx_boosted(ctx, true); + } + + data->in_rx_mode = true; + data->tx_active = false; +} + +/* ── DIO1 IRQ handler (work queue, thread context) ──────────────────── */ + +static void lr11xx_dio1_callback(void *user_data); + +static void lr11xx_dio1_work_handler(struct k_work *work) +{ + struct lr11xx_data *data = CONTAINER_OF(work, struct lr11xx_data, + dio1_work); + const struct lr11xx_config *cfg = data->dev->config; + void *ctx = &data->hal_ctx; + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + + lr11xx_system_irq_mask_t irq; + lr11xx_system_get_and_clear_irq_status(ctx, &irq); + + LOG_DBG("DIO1 IRQ: 0x%08x tx=%d", irq, data->tx_active); + + /* ── RX done ── */ + if (irq & LR11XX_SYSTEM_IRQ_RX_DONE) { + lr11xx_radio_rx_buffer_status_t rx_stat; + lr11xx_radio_get_rx_buffer_status(ctx, &rx_stat); + + if (rx_stat.pld_len_in_bytes > 0 && + rx_stat.pld_len_in_bytes <= 255) { + lr11xx_radio_pkt_status_lora_t pkt_stat; + lr11xx_radio_get_lora_pkt_status(ctx, &pkt_stat); + + lr11xx_regmem_read_buffer8(ctx, data->rx_buf, + rx_stat.buffer_start_pointer, + rx_stat.pld_len_in_bytes); + + /* Restart RX BEFORE callback (matches SX126x pattern) */ + lr11xx_start_rx(data, cfg); + + k_mutex_unlock(&data->spi_mutex); + + /* Fire callback outside mutex */ + if (data->async_rx_cb) { + data->async_rx_cb(data->dev, data->rx_buf, + rx_stat.pld_len_in_bytes, + pkt_stat.rssi_pkt_in_dbm, + pkt_stat.snr_pkt_in_db, + data->async_rx_user_data); + } + return; + } + + LOG_WRN("RX: invalid len %d", rx_stat.pld_len_in_bytes); + lr11xx_start_rx(data, cfg); + } + + /* ── TX done ── */ + if (irq & LR11XX_SYSTEM_IRQ_TX_DONE) { + LOG_INF("TX done"); + data->tx_active = false; + + /* Restart RX */ + lr11xx_start_rx(data, cfg); + + /* Raise TX signal */ + if (data->tx_signal) { + k_poll_signal_raise(data->tx_signal, 0); + } + } + + /* ── Timeout ── */ + if (irq & LR11XX_SYSTEM_IRQ_TIMEOUT) { + LOG_DBG("Timeout IRQ"); + if (!data->tx_active) { + lr11xx_start_rx(data, cfg); + } + } + + /* ── CRC / Header error ── */ + if (irq & (LR11XX_SYSTEM_IRQ_CRC_ERROR | + LR11XX_SYSTEM_IRQ_HEADER_ERROR)) { + LOG_WRN("RX error: CRC=%d HDR=%d", + (irq & LR11XX_SYSTEM_IRQ_CRC_ERROR) ? 1 : 0, + (irq & LR11XX_SYSTEM_IRQ_HEADER_ERROR) ? 1 : 0); + + /* LR1110 errata: header error can shift the RX buffer + * pointer by 4 bytes. Standby clears the shift. */ + if (irq & LR11XX_SYSTEM_IRQ_HEADER_ERROR) { + lr11xx_system_set_standby(ctx, + LR11XX_SYSTEM_STANDBY_CFG_RC); + } + + if (!data->tx_active) { + lr11xx_start_rx(data, cfg); + } + + k_mutex_unlock(&data->spi_mutex); + + /* Notify callback with NULL data for error counting */ + if (data->async_rx_cb) { + data->async_rx_cb(data->dev, NULL, 0, 0, 0, + data->async_rx_user_data); + } + return; + } + + k_mutex_unlock(&data->spi_mutex); +} + +/* HAL DIO1 callback → submits work */ +static void lr11xx_dio1_callback(void *user_data) +{ + struct lr11xx_data *data = (struct lr11xx_data *)user_data; + k_work_submit(&data->dio1_work); +} + +/* Forward declaration — hw_init is defined after driver API functions */ +static int lr11xx_hw_init(struct lr11xx_data *data, + const struct lr11xx_config *cfg); + +/* ── Driver API: config ─────────────────────────────────────────────── */ + +static int lr11xx_lora_config(const struct device *dev, + struct lora_modem_config *config) +{ + struct lr11xx_data *data = dev->data; + const struct lr11xx_config *cfg = dev->config; + + /* Deferred hardware init — first config() triggers the heavy work */ + if (!data->hw_initialized) { + int ret = lr11xx_hw_init(data, cfg); + if (ret != 0) { + LOG_ERR("Hardware init failed: %d", ret); + return ret; + } + } + + memcpy(&data->modem_cfg, config, sizeof(*config)); + data->configured = true; + + LOG_INF("config: %uHz SF%d BW%d CR%d pwr=%d tx=%d", + config->frequency, config->datarate, config->bandwidth, + config->coding_rate, config->tx_power, config->tx); + + return 0; +} + +/* ── Driver API: airtime ────────────────────────────────────────────── */ + +static uint32_t lr11xx_lora_airtime(const struct device *dev, + uint32_t data_len) +{ + struct lr11xx_data *data = dev->data; + struct lora_modem_config *mc = &data->modem_cfg; + + uint8_t sf = (uint8_t)mc->datarate; + float bw = bw_enum_to_khz(mc->bandwidth) * 1000.0f; + uint8_t cr = (uint8_t)mc->coding_rate + 4; + + float ts = (float)(1 << sf) / bw; + int de = (sf >= 11 && bw <= 125000.0f) ? 1 : 0; + float n_payload = 8.0f + fmaxf( + ceilf((8.0f * data_len - 4.0f * sf + 28.0f + 16.0f) / + (4.0f * (sf - 2.0f * de))) * cr, + 0.0f); + float t_preamble = (mc->preamble_len + 4.25f) * ts; + float t_payload = n_payload * ts; + + return (uint32_t)((t_preamble + t_payload) * 1000.0f); +} + +/* ── Driver API: send_async ─────────────────────────────────────────── */ + +static int lr11xx_lora_send_async(const struct device *dev, + uint8_t *buf, uint32_t data_len, + struct k_poll_signal *async) +{ + struct lr11xx_data *data = dev->data; + const struct lr11xx_config *cfg = dev->config; + void *ctx = &data->hal_ctx; + + if (!data->configured) return -EINVAL; + if (data->tx_active) return -EBUSY; + if (data_len > 255 || data_len == 0) return -EINVAL; + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + + /* Cancel RX */ + data->async_rx_cb = NULL; + data->in_rx_mode = false; + + lr11xx_hal_disable_dio1_irq(&data->hal_ctx); + + /* Standby — wake from sleep if needed */ + data->hal_ctx.radio_is_sleeping = true; + lr11xx_status_t rc = lr11xx_system_set_standby(ctx, + LR11XX_SYSTEM_STANDBY_CFG_RC); + if (rc != LR11XX_STATUS_OK) { + LOG_ERR("TX standby failed — HW reset"); + lr11xx_hardware_reset(data, cfg); + } + + /* Apply TX config */ + lr11xx_apply_modem_config(data, cfg, true); + + /* Set TX packet length */ + lr11xx_radio_pkt_params_lora_t pkt = { + .preamble_len_in_symb = data->modem_cfg.preamble_len, + .header_type = LR11XX_RADIO_LORA_PKT_EXPLICIT, + .pld_len_in_bytes = (uint8_t)data_len, + .crc = data->modem_cfg.packet_crc_disable + ? LR11XX_RADIO_LORA_CRC_OFF + : LR11XX_RADIO_LORA_CRC_ON, + .iq = data->modem_cfg.iq_inverted + ? LR11XX_RADIO_LORA_IQ_INVERTED + : LR11XX_RADIO_LORA_IQ_STANDARD, + }; + lr11xx_radio_set_lora_pkt_params(ctx, &pkt); + + /* Write TX buffer */ + lr11xx_regmem_write_buffer8(ctx, buf, data_len); + + /* Clear IRQ, enable DIO1 */ + lr11xx_system_clear_irq_status(ctx, LR11XX_SYSTEM_IRQ_ALL_MASK); + lr11xx_hal_enable_dio1_irq(&data->hal_ctx); + + /* Store signal and start TX */ + data->tx_signal = async; + data->tx_active = true; + lr11xx_radio_set_tx(ctx, 5000); + + k_mutex_unlock(&data->spi_mutex); + + LOG_INF("TX started: len=%u", data_len); + return 0; +} + +/* ── Driver API: send (sync) ────────────────────────────────────────── */ + +static int lr11xx_lora_send(const struct device *dev, + uint8_t *buf, uint32_t data_len) +{ + struct k_poll_signal done = K_POLL_SIGNAL_INITIALIZER(done); + struct k_poll_event evt = K_POLL_EVENT_INITIALIZER( + K_POLL_TYPE_SIGNAL, K_POLL_MODE_NOTIFY_ONLY, &done); + + int ret = lr11xx_lora_send_async(dev, buf, data_len, &done); + if (ret < 0) return ret; + + uint32_t air_time = lr11xx_lora_airtime(dev, data_len); + ret = k_poll(&evt, 1, K_MSEC(2 * air_time + 1000)); + if (ret < 0) { + LOG_ERR("TX sync timeout"); + return ret; + } + + return 0; +} + +/* ── Driver API: recv_async ─────────────────────────────────────────── */ + +static int lr11xx_lora_recv_async(const struct device *dev, + lora_recv_cb cb, void *user_data) +{ + struct lr11xx_data *data = dev->data; + const struct lr11xx_config *cfg = dev->config; + + /* NULL cb = cancel RX */ + if (cb == NULL) { + k_mutex_lock(&data->spi_mutex, K_FOREVER); + data->async_rx_cb = NULL; + data->async_rx_user_data = NULL; + data->in_rx_mode = false; + k_mutex_unlock(&data->spi_mutex); + return 0; + } + + if (!data->configured) return -EINVAL; + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + + data->async_rx_cb = cb; + data->async_rx_user_data = user_data; + + lr11xx_start_rx(data, cfg); + + k_mutex_unlock(&data->spi_mutex); + + LOG_DBG("recv_async started%s%s", + data->rx_duty_cycle_enabled ? " (duty cycle)" : "", + data->rx_boost_enabled ? " (boosted)" : ""); + + return 0; +} + +/* ── Driver API: recv (sync) ─────────────────────────────────────────── */ + +static int lr11xx_lora_recv(const struct device *dev, uint8_t *buf, + uint8_t size, k_timeout_t timeout, + int16_t *rssi, int8_t *snr) +{ + ARG_UNUSED(dev); + ARG_UNUSED(buf); + ARG_UNUSED(size); + ARG_UNUSED(timeout); + ARG_UNUSED(rssi); + ARG_UNUSED(snr); + return -ENOTSUP; +} + +/* ── LR11xx extension API ───────────────────────────────────────────── */ + +int16_t lr11xx_get_rssi_inst(const struct device *dev) +{ + struct lr11xx_data *data = dev->data; + int8_t rssi; + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + lr11xx_radio_get_rssi_inst(&data->hal_ctx, &rssi); + k_mutex_unlock(&data->spi_mutex); + + return (int16_t)rssi; +} + +bool lr11xx_is_receiving(const struct device *dev) +{ + struct lr11xx_data *data = dev->data; + + /* Non-blocking — skip if SPI busy */ + if (k_mutex_lock(&data->spi_mutex, K_NO_WAIT) != 0) { + return false; + } + + lr11xx_system_irq_mask_t irq; + lr11xx_system_get_irq_status(&data->hal_ctx, &irq); + k_mutex_unlock(&data->spi_mutex); + + return (irq & (LR11XX_SYSTEM_IRQ_PREAMBLE_DETECTED | + LR11XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID)) != 0; +} + +void lr11xx_set_rx_duty_cycle(const struct device *dev, bool enable) +{ + struct lr11xx_data *data = dev->data; + + data->rx_duty_cycle_enabled = enable; + LOG_INF("RX duty cycle %s", enable ? "enabled" : "disabled"); + + if (data->in_rx_mode) { + k_mutex_lock(&data->spi_mutex, K_FOREVER); + if (enable) { + lr11xx_apply_rx_duty_cycle(data); + } else { + lr11xx_radio_set_rx(&data->hal_ctx, 0xFFFFFF); + if (data->rx_boost_enabled) { + lr11xx_radio_cfg_rx_boosted(&data->hal_ctx, + true); + } + } + k_mutex_unlock(&data->spi_mutex); + } +} + +void lr11xx_set_rx_boost(const struct device *dev, bool enable) +{ + struct lr11xx_data *data = dev->data; + + data->rx_boost_enabled = enable; + LOG_INF("RX boost %s", enable ? "enabled" : "disabled"); + + if (data->in_rx_mode) { + k_mutex_lock(&data->spi_mutex, K_FOREVER); + lr11xx_radio_cfg_rx_boosted(&data->hal_ctx, enable); + k_mutex_unlock(&data->spi_mutex); + } +} + +uint32_t lr11xx_get_random(const struct device *dev) +{ + struct lr11xx_data *data = dev->data; + uint32_t random = 0; + + k_mutex_lock(&data->spi_mutex, K_FOREVER); + lr11xx_system_get_random_number(&data->hal_ctx, &random); + k_mutex_unlock(&data->spi_mutex); + + return random; +} + +/* ── Deferred hardware init (runs on first lora_config call) ────────── */ + +static int lr11xx_hw_init(struct lr11xx_data *data, + const struct lr11xx_config *cfg) +{ + void *ctx = &data->hal_ctx; + + LOG_INF("LR11xx hardware init starting"); + + /* Hardware reset */ + lr11xx_hal_status_t hal_rc = lr11xx_hal_reset(ctx); + if (hal_rc != LR11XX_HAL_STATUS_OK) { + LOG_ERR("LR11xx reset failed"); + return -EIO; + } + + /* Get version */ + lr11xx_system_version_t ver; + lr11xx_status_t st = lr11xx_system_get_version(ctx, &ver); + if (st == LR11XX_STATUS_OK) { + LOG_INF("LR11xx HW:0x%02X Type:0x%02X FW:0x%04X", + ver.hw, ver.type, ver.fw); + } else { + LOG_ERR("Get version failed"); + return -EIO; + } + + /* TCXO */ + if (cfg->tcxo_voltage_mv > 0) { + lr11xx_system_set_tcxo_mode(ctx, + get_tcxo_voltage(cfg->tcxo_voltage_mv), + 164); + LOG_DBG("TCXO: %dmV", cfg->tcxo_voltage_mv); + } + + /* DC-DC */ + lr11xx_system_set_reg_mode(ctx, LR11XX_SYSTEM_REG_MODE_DCDC); + + /* RF switch */ + lr11xx_system_rfswitch_cfg_t rfsw = { + .enable = cfg->rfswitch_enable, + .standby = cfg->rfswitch_standby, + .rx = cfg->rfswitch_rx, + .tx = cfg->rfswitch_tx, + .tx_hp = cfg->rfswitch_tx_hp, + .tx_hf = 0, + .gnss = cfg->rfswitch_gnss, + .wifi = 0, + }; + lr11xx_system_set_dio_as_rf_switch(ctx, &rfsw); + LOG_INF("RF switch: en=0x%02x rx=0x%02x tx=0x%02x txhp=0x%02x", + rfsw.enable, rfsw.rx, rfsw.tx, rfsw.tx_hp); + + /* Calibrate */ + lr11xx_system_calibrate(ctx, 0x3F); + LOG_INF("Calibration OK"); + + /* LoRa mode */ + lr11xx_radio_set_pkt_type(ctx, LR11XX_RADIO_PKT_TYPE_LORA); + + /* Enable DIO1 */ + lr11xx_hal_enable_dio1_irq(&data->hal_ctx); + + /* Set default boost from DTS */ + data->rx_boost_enabled = cfg->rx_boosted; + + data->hw_initialized = true; + LOG_INF("LR11xx driver ready"); + return 0; +} + +/* ── Driver init (lightweight — runs at POST_KERNEL) ────────────────── */ + +static int lr11xx_lora_init(const struct device *dev) +{ + struct lr11xx_data *data = dev->data; + const struct lr11xx_config *cfg = dev->config; + int ret; + + data->dev = dev; + data->hw_initialized = false; + + k_mutex_init(&data->spi_mutex); + k_work_init(&data->dio1_work, lr11xx_dio1_work_handler); + + /* Check SPI bus */ + if (!spi_is_ready_dt(&cfg->bus)) { + LOG_ERR("SPI bus not ready"); + return -ENODEV; + } + + /* Fill HAL context from DTS config */ + memset(&data->hal_ctx, 0, sizeof(data->hal_ctx)); + data->hal_ctx.spi_dev = cfg->bus.bus; + data->hal_ctx.spi_cfg = cfg->bus.config; + /* Override CS — we control NSS manually via gpio_pin_set_dt, + * the SPI controller must NOT drive CS. The DTS cs-gpios on + * the SPI bus assigns the pin, but our HAL does manual NSS. + * Clear BOTH cs_is_gpio AND the port pointer so the SPI framework's + * spi_context_cs_control() does not try to drive CS (which would + * NULL-deref on the cleared port pointer). */ + data->hal_ctx.spi_cfg.cs.cs_is_gpio = false; + data->hal_ctx.spi_cfg.cs.gpio.port = NULL; + data->hal_ctx.nss.port = cfg->bus.config.cs.gpio.port; + data->hal_ctx.nss.pin = cfg->bus.config.cs.gpio.pin; + data->hal_ctx.nss.dt_flags = cfg->bus.config.cs.gpio.dt_flags; + data->hal_ctx.reset = cfg->reset; + data->hal_ctx.busy = cfg->busy; + data->hal_ctx.dio1 = cfg->dio1; + data->hal_ctx.tcxo_voltage_mv = cfg->tcxo_voltage_mv; + data->hal_ctx.tcxo_startup_us = cfg->tcxo_startup_delay_ms * 1000; + data->hal_ctx.radio_is_sleeping = false; + + /* Init HAL GPIOs */ + ret = lr11xx_hal_init(&data->hal_ctx); + if (ret != 0) { + LOG_ERR("HAL init failed: %d", ret); + return ret; + } + + /* Set DIO1 callback — routes through HAL work queue to our handler */ + lr11xx_hal_set_dio1_callback(&data->hal_ctx, lr11xx_dio1_callback, + data); + + LOG_INF("LR11xx driver registered (hw init deferred to first config)"); + return 0; +} + +/* ── Device instantiation ───────────────────────────────────────────── */ + +static DEVICE_API(lora, lr11xx_lora_api) = { + .config = lr11xx_lora_config, + .airtime = lr11xx_lora_airtime, + .send = lr11xx_lora_send, + .send_async = lr11xx_lora_send_async, + .recv = lr11xx_lora_recv, + .recv_async = lr11xx_lora_recv_async, +}; + +#define LR11XX_INIT(n) \ + static const struct lr11xx_config lr11xx_config_##n = { \ + .bus = SPI_DT_SPEC_INST_GET(n, \ + SPI_WORD_SET(8) | SPI_OP_MODE_MASTER | \ + SPI_TRANSFER_MSB, 0), \ + .reset = GPIO_DT_SPEC_INST_GET(n, reset_gpios), \ + .busy = GPIO_DT_SPEC_INST_GET(n, busy_gpios), \ + .dio1 = GPIO_DT_SPEC_INST_GET(n, dio1_gpios), \ + .tcxo_voltage_mv = \ + DT_INST_PROP_OR(n, tcxo_voltage_mv, 0), \ + .tcxo_startup_delay_ms = \ + DT_INST_PROP_OR(n, tcxo_startup_delay_ms, 5), \ + .rx_boosted = DT_INST_PROP(n, rx_boosted), \ + .rfswitch_enable = DT_INST_PROP_OR(n, rfswitch_enable, 0),\ + .rfswitch_standby = DT_INST_PROP_OR(n, rfswitch_standby,0),\ + .rfswitch_rx = DT_INST_PROP_OR(n, rfswitch_rx, 0), \ + .rfswitch_tx = DT_INST_PROP_OR(n, rfswitch_tx, 0), \ + .rfswitch_tx_hp = DT_INST_PROP_OR(n, rfswitch_tx_hp, 0),\ + .rfswitch_gnss = DT_INST_PROP_OR(n, rfswitch_gnss, 0), \ + .pa_hp_sel = DT_INST_PROP_OR(n, pa_hp_sel, 7), \ + .pa_duty_cycle = DT_INST_PROP_OR(n, pa_duty_cycle, 4), \ + }; \ + static struct lr11xx_data lr11xx_data_##n; \ + DEVICE_DT_INST_DEFINE(n, lr11xx_lora_init, NULL, \ + &lr11xx_data_##n, &lr11xx_config_##n, \ + POST_KERNEL, CONFIG_LORA_INIT_PRIORITY, \ + &lr11xx_lora_api); + +DT_INST_FOREACH_STATUS_OKAY(LR11XX_INIT) diff --git a/zephcore/patches/zephyr/drivers/lora/lr11xx/lr11xx_lora.h b/zephcore/patches/zephyr/drivers/lora/lr11xx/lr11xx_lora.h new file mode 100644 index 0000000..813a3d7 --- /dev/null +++ b/zephcore/patches/zephyr/drivers/lora/lr11xx/lr11xx_lora.h @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * LR11xx Zephyr LoRa driver — extension API + * + * Functions extending the standard Zephyr lora_driver_api with + * LR11xx-specific features (duty cycle, RX boost, RSSI readout, + * preamble detection). + */ + +#ifndef LR11XX_LORA_H +#define LR11XX_LORA_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Get instantaneous RSSI (for noise floor calibration) + * + * Reads the current RSSI from the radio while in RX mode. + * Protected by internal SPI mutex. + * + * @param dev LoRa device + * @return RSSI in dBm, or -128 on error + */ +int16_t lr11xx_get_rssi_inst(const struct device *dev); + +/** + * @brief Check if radio is actively receiving a packet + * + * Checks IRQ status for preamble/sync word detection. + * Uses non-blocking mutex — returns false if SPI is busy. + * + * @param dev LoRa device + * @return true if preamble or sync word detected + */ +bool lr11xx_is_receiving(const struct device *dev); + +/** + * @brief Enable/disable RX duty cycle mode + * + * When enabled, the radio alternates between RX and sleep using the + * RadioLib preamble detection algorithm. Saves ~60-70% RX current. + * + * Takes effect on the next RX start (recv_async or after TX). + * + * @param dev LoRa device + * @param enable true to enable, false for continuous RX + */ +void lr11xx_set_rx_duty_cycle(const struct device *dev, bool enable); + +/** + * @brief Enable/disable RX boosted mode + * + * Boosted mode increases LNA gain for +3dB sensitivity at +2mA cost. + * + * @param dev LoRa device + * @param enable true to enable boost + */ +void lr11xx_set_rx_boost(const struct device *dev, bool enable); + +/** + * @brief Get a random number from the radio + * + * Uses LR11xx hardware RNG. + * + * @param dev LoRa device + * @return Random 32-bit value + */ +uint32_t lr11xx_get_random(const struct device *dev); + +#ifdef __cplusplus +} +#endif + +#endif /* LR11XX_LORA_H */ diff --git a/zephcore/patches/zephyr/dts/bindings/lora/semtech,lr1110.yaml b/zephcore/patches/zephyr/dts/bindings/lora/semtech,lr1110.yaml new file mode 100644 index 0000000..15afbaf --- /dev/null +++ b/zephcore/patches/zephyr/dts/bindings/lora/semtech,lr1110.yaml @@ -0,0 +1,86 @@ +# Copyright (c) 2025 ZephCore +# SPDX-License-Identifier: Apache-2.0 + +description: | + Semtech LR1110/LR1120/LR1121 LoRa transceiver. + Uses native Semtech lr11xx_driver SDK with Zephyr LoRa driver API. + +compatible: "semtech,lr1110" + +include: spi-device.yaml + +properties: + reset-gpios: + type: phandle-array + required: true + description: GPIO connected to NRESET (active-low). + + busy-gpios: + type: phandle-array + required: true + description: GPIO connected to BUSY (high = busy). + + dio1-gpios: + type: phandle-array + required: true + description: GPIO connected to DIO1 (IRQ line, active-high). + + tcxo-voltage-mv: + type: int + default: 0 + description: | + TCXO supply voltage in millivolts. + 0 = no TCXO. Common: 1600 (1.6V), 1800 (1.8V), 3300 (3.3V). + + tcxo-startup-delay-ms: + type: int + default: 5 + description: TCXO startup stabilization time in milliseconds. + + rx-boosted: + type: boolean + description: | + Enable RX boosted mode (+3dB sensitivity, +2mA power). + + # RF switch configuration (DIO5-DIO8 bitmask) + # Bit 0 = RFSW0 (DIO5), Bit 1 = RFSW1 (DIO6), + # Bit 2 = RFSW2 (DIO7), Bit 3 = RFSW3 (DIO8) + rfswitch-enable: + type: int + default: 0 + description: RF switch DIO enable bitmask (which DIOs are RF switches). + + rfswitch-standby: + type: int + default: 0 + description: RF switch state in standby mode. + + rfswitch-rx: + type: int + default: 0 + description: RF switch state in RX mode. + + rfswitch-tx: + type: int + default: 0 + description: RF switch state in TX mode. + + rfswitch-tx-hp: + type: int + default: 0 + description: RF switch state in TX high-power mode. + + rfswitch-gnss: + type: int + default: 0 + description: RF switch state in GNSS mode. + + pa-hp-sel: + type: int + default: 7 + description: PA HP selection (0-7). 7 = max power for HP PA. + + pa-duty-cycle: + type: int + default: 4 + description: PA duty cycle (0-7). Controls PA output power curve. diff --git a/zephcore/patches/zephyr/scripts/west_commands/blobs.py b/zephcore/patches/zephyr/scripts/west_commands/blobs.py new file mode 100644 index 0000000..615ba60 --- /dev/null +++ b/zephcore/patches/zephyr/scripts/west_commands/blobs.py @@ -0,0 +1,346 @@ +# Copyright (c) 2022 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import os +import re +import shutil +import sys +import textwrap +from pathlib import Path +from urllib.parse import urlparse + +from west.commands import WestCommand + +from zephyr_ext_common import ZEPHYR_BASE + +sys.path.append(os.fspath(Path(__file__).parent.parent)) +import zephyr_module + + +class Blobs(WestCommand): + DEFAULT_LIST_FMT = '{module} {status} {path} {type} {abspath}' + + def __init__(self): + super().__init__( + 'blobs', + # Keep this in sync with the string in west-commands.yml. + 'work with binary blobs', + 'Work with binary blobs', + accepts_unknown_args=False, + ) + + def do_add_parser(self, parser_adder): + parser = parser_adder.add_parser( + self.name, + help=self.help, + formatter_class=argparse.RawDescriptionHelpFormatter, + description=self.description, + epilog=textwrap.dedent(f'''\ + FORMAT STRINGS + -------------- + + Blobs are listed using a Python 3 format string. Arguments + to the format string are accessed by name. + + The default format string is: + + "{self.DEFAULT_LIST_FMT}" + + The following arguments are available: + + - module: name of the module that contains this blob + - abspath: blob absolute path + - status: short status (A: present, M: hash failure, D: not present) + - path: blob local path from /zephyr/blobs/ + - sha256: blob SHA256 hash in hex + - type: type of blob + - version: version string + - license_path: path to the license file for the blob + - license-abspath: absolute path to the license file for the blob + - click-through: need license click-through or not + - uri: URI to the remote location of the blob + - description: blob text description + - doc-url: URL to the documentation for this blob + '''), + ) + + # Remember to update west-completion.bash if you add or remove + # flags + parser.add_argument( + 'subcmd', nargs=1, choices=['list', 'fetch', 'clean'], help='sub-command to execute' + ) + + parser.add_argument( + 'modules', + metavar='MODULE', + nargs='*', + help='''zephyr modules to operate on; + all modules will be used if not given''', + ) + + group = parser.add_argument_group('west blob list options') + group.add_argument( + '-f', + '--format', + help='''format string to use to list each blob; + see FORMAT STRINGS below''', + ) + + group = parser.add_argument_group('west blobs fetch options') + group.add_argument( + '-l', + '--allow-regex', + help='''Regex pattern to apply to the blob local path. + Only local paths matching this regex will be fetched. + Note that local paths are relative to the module directory''', + ) + group.add_argument( + '-a', + '--auto-accept', + action='store_true', + help='''auto accept license if the fetching needs click-through''', + ) + group.add_argument( + '--cache-dirs', + help='''Semicolon-separated list of directories to search for cached + blobs before downloading. Cache files may use the original + filename or be suffixed with `.`.''', + ) + group.add_argument( + '--auto-cache', + help='''Path to a directory that is automatically populated when a blob + is downloaded. Cached blobs are stored using the original + filename suffixed with `.`.''', + ) + + return parser + + def get_blobs(self, args): + blobs = [] + modules = args.modules + all_modules = zephyr_module.parse_modules(ZEPHYR_BASE, self.manifest) + all_names = [m.meta.get('name', None) for m in all_modules] + + unknown = set(modules) - set(all_names) + + if len(unknown): + self.die(f'Unknown module(s): {unknown}') + + for module in all_modules: + # Filter by module + module_name = module.meta.get('name', None) + if len(modules) and module_name not in modules: + continue + + blobs += zephyr_module.process_blobs(module.project, module.meta) + + return blobs + + def list(self, args): + blobs = self.get_blobs(args) + fmt = args.format or self.DEFAULT_LIST_FMT + for blob in blobs: + self.inf(fmt.format(**blob)) + + def ensure_folder(self, path): + path.parent.mkdir(parents=True, exist_ok=True) + + def handle_auto_cache(self, blob, auto_cache_dir) -> Path: + """ + This function guarantees that a given blob exists in the auto-cache. + It first checks whether the blob is already present. If so, it + returns the path of this cached blob. If the blob is not yet cached, + the blob is downloaded into the auto-cache directory and the path of + the freshly cached blob is returned. + """ + cached_blob = self.get_cached_blob(blob, [auto_cache_dir]) + if cached_blob: + return cached_blob + name = Path(blob['path']).name + sha256 = blob['sha256'] + self.download_blob(blob, auto_cache_dir / f'{name}.{sha256}') + cached_blob = self.get_cached_blob(blob, [auto_cache_dir]) + assert cached_blob, f'Blob {name} still not cached in auto-cache.' + return cached_blob + + def get_cached_blob(self, blob, cache_dirs: list) -> Path | None: + """ + Look for a cached blob in the provided cache directories. + A blob may be stored using either its original name or suffixed with + its SHA256 hash (e.g. "."). + Return the first matching path, or None if not found. + """ + name = Path(blob['path']).name + sha256 = blob["sha256"] + candidate_names = [ + f"{name}.{sha256}", # suffixed version + name, # original blob name + ] + + for cache_dir in cache_dirs: + if not cache_dir.exists(): + continue + for name in candidate_names: + candidate_path = cache_dir / name + if ( + zephyr_module.get_blob_status(candidate_path, sha256) + == zephyr_module.BLOB_PRESENT + ): + return candidate_path + return None + + def download_blob(self, blob, path): + '''Download a blob from its url to a given path.''' + url = blob['url'] + scheme = urlparse(url).scheme + self.dbg(f'Fetching blob from url {url} with {scheme} to path: {path}') + import fetchers + + fetcher = fetchers.get_fetcher_cls(scheme) + self.dbg(f'Found fetcher: {fetcher}') + inst = fetcher() + self.ensure_folder(path) + inst.fetch(url, path) + + def fetch_blob(self, args, blob): + """ + Ensures that the specified blob is available at its path. + If caching is enabled and the blob exists in the cache, it is copied + from there. Otherwise, the blob is downloaded from its URL and placed + at the target path. + """ + path = Path(blob['abspath']) + + # collect existing cache dirs specified as args, otherwise from west config + cache_dirs = args.cache_dirs + auto_cache_dir = args.auto_cache + if self.has_config: + if cache_dirs is None: + cache_dirs = self.config.get('blobs.cache-dirs') + if auto_cache_dir is None: + auto_cache_dir = self.config.get('blobs.auto-cache') + + # expand user home for each cache directory + if auto_cache_dir is not None: + auto_cache_dir = Path(auto_cache_dir).expanduser() + if cache_dirs is not None: + cache_dirs = [Path(p).expanduser() for p in cache_dirs.split(';') if p] + + # search for cached blob in the cache directories + cached_blob = self.get_cached_blob(blob, cache_dirs or []) + + # If blob is not found in cache directories: Use auto-cache if enabled + if not cached_blob and auto_cache_dir: + cached_blob = self.handle_auto_cache(blob, auto_cache_dir) + + # Copy blob if it is cached, otherwise download it + if cached_blob: + self.dbg(f'Copy cached blob: {cached_blob}') + self.ensure_folder(path) + shutil.copy(cached_blob, path) + else: + self.download_blob(blob, path) + + # Compare the checksum of a file we've just downloaded + # to the digest in blob metadata, warn user if they differ. + def verify_blob(self, blob) -> bool: + self.dbg(f"Verifying blob {blob['module']}: {blob['abspath']}") + + status = zephyr_module.get_blob_status(blob['abspath'], blob['sha256']) + if status == zephyr_module.BLOB_OUTDATED: + self.err( + textwrap.dedent( + f'''\ + The checksum of the downloaded file does not match that + in the blob metadata: + - if it is not certain that the download was successful, + try running 'west blobs fetch {blob['module']}' + to re-download the file + - if the error persists, please consider contacting + the maintainers of the module so that they can check + the corresponding blob metadata + + Module: {blob['module']} + Blob: {blob['path']} + URL: {blob['url']} + Info: {blob['description']}''' + ) + ) + return False + return True + + def fetch(self, args): + bad_checksum_count = 0 + blobs = self.get_blobs(args) + for blob in blobs: + if blob['status'] == zephyr_module.BLOB_PRESENT: + self.dbg(f"Blob {blob['module']}: {blob['abspath']} is up to date") + continue + + # if args.allow_regex is set, use it to filter the blob by path + if args.allow_regex and not re.match(args.allow_regex, blob['path']): + self.dbg( + f"Blob {blob['module']}: {blob['abspath']} does not match regex " + f"'{args.allow_regex}', skipping" + ) + continue + self.inf(f"Fetching blob {blob['module']}: {blob['abspath']}") + + if blob.get('click-through') and not args.auto_accept: + while True: + user_input = input( + "For this blob, need to read and accept " + "license to continue. Read it?\n" + "Please type 'y' or 'n' and press enter to confirm: " + ) + if user_input.upper() == "Y" or user_input.upper() == "N": + break + + if user_input.upper() != "Y": + self.wrn('Skip fetching this blob.') + continue + + with open(blob['license-abspath'], encoding="utf-8") as license_file: + license_content = license_file.read() + print(license_content) + + while True: + user_input = input( + "Accept license to continue?\n" + "Please type 'y' or 'n' and press enter to confirm: " + ) + if user_input.upper() == "Y" or user_input.upper() == "N": + break + + if user_input.upper() != "Y": + self.wrn('Skip fetching this blob.') + continue + + self.fetch_blob(args, blob) + if not self.verify_blob(blob): + bad_checksum_count += 1 + + if bad_checksum_count: + self.err(f"{bad_checksum_count} blobs have bad checksums") + sys.exit(os.EX_DATAERR) + + def clean(self, args): + blobs = self.get_blobs(args) + for blob in blobs: + if blob['status'] == zephyr_module.BLOB_NOT_PRESENT: + self.dbg(f"Blob {blob['module']}: {blob['abspath']} not in filesystem") + continue + self.inf(f"Deleting blob {blob['module']}: {blob['status']} {blob['abspath']}") + blob['abspath'].unlink() + + def do_run(self, args, _): + self.dbg(f"subcmd: '{args.subcmd[0]}' modules: {args.modules}") + + subcmd = getattr(self, args.subcmd[0]) + + if args.subcmd[0] != 'list' and args.format is not None: + self.die('unexpected --format argument; this is a "west blobs list" option') + + subcmd(args) diff --git a/zephcore/prj.conf b/zephcore/prj.conf new file mode 100644 index 0000000..474cb97 --- /dev/null +++ b/zephcore/prj.conf @@ -0,0 +1,30 @@ +# ZephCore - Main Project Configuration +# +# This file contains ONLY console/logging setup and includes common configs. +# Platform-specific configs are in boards/common/_common.conf +# Board-specific configs (pins only) are in boards/.conf +# +# Include hierarchy: +# prj.conf (this file - logging/console) +# → boards/common/zephcore_common.conf (app features, crypto, sensors) +# → boards/common/_common.conf (BLE, flash, storage) +# → boards/.conf (pins, board-specific features) + +# ========== Console & Logging ========== +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y +CONFIG_UART_INTERRUPT_DRIVEN=y +CONFIG_RING_BUFFER=y + +# Logging subsystem — enabled by default (prod.conf disables with CONFIG_LOG=n). +# Log tuning (buffer size, backends, etc.) is in boards/common/logging.conf, +# which CMake auto-includes only when prod.conf is NOT active. +CONFIG_LOG=y + +# ========== Power Management ========== +# Note: nRF52840 automatically uses WFI (Wait For Interrupt) in idle thread. +# Power savings come from event-driven design: CPU sleeps in WFI whenever +# k_event_wait() blocks. GPS standby uses GPIO power control (not PM_DEVICE). +# CONFIG_PM_DEVICE intentionally DISABLED — it enables SYSTEM_MANAGED PM which +# auto-suspends all devices (including GNSS) during idle, calling modem_chat +# from an unexpected context and potentially deadlocking the system. diff --git a/zephcore/src/Dispatcher.cpp b/zephcore/src/Dispatcher.cpp new file mode 100644 index 0000000..ffb2d92 --- /dev/null +++ b/zephcore/src/Dispatcher.cpp @@ -0,0 +1,426 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Dispatcher implementation + */ + +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_dispatcher, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); + +#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING) +/* Payload types for [src -> dest] logging */ +#define PAYLOAD_TYPE_REQ 0x00 +#define PAYLOAD_TYPE_RESPONSE 0x01 +#define PAYLOAD_TYPE_TXT_MSG 0x02 +#define PAYLOAD_TYPE_PATH 0x08 +#endif + +namespace mesh { + +#define MAX_RX_DELAY_MILLIS 32000 +#define NOISE_FLOOR_CALIB_INTERVAL 2000 + +Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr) + : _radio(&radio), _ms(&ms), _mgr(&mgr) +{ + outbound = nullptr; + total_air_time = rx_air_time = 0; + next_tx_time = 0; + cad_busy_start = 0; + next_floor_calib_time = next_agc_reset_time = 0; + _err_flags = 0; + _duty_cycle.init(0); + radio_nonrx_start = 0; + prev_isrecv_mode = true; + n_sent_flood = n_sent_direct = 0; + n_recv_flood = n_recv_direct = 0; +} + +void Dispatcher::begin() +{ + n_sent_flood = n_sent_direct = 0; + n_recv_flood = n_recv_direct = 0; + _err_flags = 0; + radio_nonrx_start = (uint32_t)_ms->getMillis(); + _radio->begin(); + prev_isrecv_mode = _radio->isInRecvMode(); + _duty_cycle.init(getDutyCyclePercent()); +} + +uint8_t Dispatcher::getDutyCyclePercent() const +{ + return 10; +} + +bool Dispatcher::isAdminPacket(const Packet *pkt) +{ + uint8_t t = pkt->getPayloadType(); + return t == PAYLOAD_TYPE_REQ || t == PAYLOAD_TYPE_RESPONSE || + t == PAYLOAD_TYPE_ANON_REQ || t == PAYLOAD_TYPE_CONTROL; +} + +int Dispatcher::calcRxDelay(float score, uint32_t air_time) const +{ + /* Lookup table: (10^(0.85 - i*0.1) - 1) for i=0..10 (score 0.0 to 1.0) + * Replaces powf() to save ~1.9KB flash. Linear interpolation between entries. */ + static const float lut[11] = { + 6.0793f, 4.6236f, 3.4674f, 2.5489f, 1.8184f, 1.2389f, + 0.7783f, 0.4125f, 0.1220f, -0.1089f, -0.2921f + }; + if (score <= 0.0f) return (int)(lut[0] * (float)air_time); + if (score >= 1.0f) return (int)(lut[10] * (float)air_time); + float idx = score * 10.0f; + int i = (int)idx; + float frac = idx - (float)i; + float val = lut[i] + frac * (lut[i + 1] - lut[i]); + return (int)(val * (float)air_time); +} + +uint32_t Dispatcher::getCADFailRetryDelay() const +{ + return 200; +} + +uint32_t Dispatcher::getCADFailMaxDuration() const +{ + return 4000; +} + +void Dispatcher::loop() +{ + if (millisHasNowPassed(next_floor_calib_time)) { + _radio->triggerNoiseFloorCalibrate(getInterferenceThreshold()); + next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL); + } + + bool is_recv = _radio->isInRecvMode(); + if (is_recv != prev_isrecv_mode) { + prev_isrecv_mode = is_recv; + if (!is_recv) { + radio_nonrx_start = (uint32_t)_ms->getMillis(); + } + } + if (!is_recv && (uint32_t)_ms->getMillis() - radio_nonrx_start > 8000) { + _err_flags |= ERR_EVENT_STARTRX_TIMEOUT; + } + + if (outbound) { + if (_radio->isSendComplete()) { + uint32_t t = (uint32_t)_ms->getMillis() - outbound_start; + total_air_time += t; + _duty_cycle.recordTx(t, (uint32_t)_ms->getMillis()); + _radio->onSendFinished(); + logTx(outbound, 2 + outbound->path_len + outbound->payload_len); + if (outbound->isRouteFlood()) { + n_sent_flood++; + } else { + n_sent_direct++; + } + releasePacket(outbound); + outbound = nullptr; + } else if (millisHasNowPassed(outbound_expiry)) { + _radio->onSendFinished(); + logTxFail(outbound, 2 + outbound->path_len + outbound->payload_len); + releasePacket(outbound); + outbound = nullptr; + } else { + return; + } + next_agc_reset_time = futureMillis(getAGCResetInterval()); + } + + if (getAGCResetInterval() > 0 && millisHasNowPassed(next_agc_reset_time)) { + _radio->resetAGC(); + next_agc_reset_time = futureMillis(getAGCResetInterval()); + } + + { + Packet *pkt = _mgr->getNextInbound((uint32_t)_ms->getMillis()); + if (pkt) { + processRecvPacket(pkt); + } + } + checkRecv(); + checkSend(); +} + +void Dispatcher::checkRecv() +{ + Packet *pkt = nullptr; + float score = 0.0f; + uint32_t air_time = 0; + + { + uint8_t raw[MAX_TRANS_UNIT + 1]; + int len = _radio->recvRaw(raw, MAX_TRANS_UNIT); + if (len > 0) { + LOG_INF("checkRecv: got raw packet len=%d", len); + logRxRaw(_radio->getLastSNR(), _radio->getLastRSSI(), raw, len); + pkt = _mgr->allocNew(); + if (pkt == nullptr) { + LOG_WRN("checkRecv: packet alloc failed"); + return; + } + LOG_INF("checkRecv: parsing packet"); + int i = 0; + pkt->header = raw[i++]; + if (pkt->hasTransportCodes()) { + memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2; + memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2; + } else { + pkt->transport_codes[0] = pkt->transport_codes[1] = 0; + } + pkt->path_len = raw[i++]; + + if (pkt->path_len > MAX_PATH_SIZE || i + pkt->path_len > len) { + LOG_WRN("checkRecv: bad path_len=%d", pkt->path_len); + _mgr->free(pkt); + pkt = nullptr; + } else { + memcpy(pkt->path, &raw[i], pkt->path_len); + i += pkt->path_len; + pkt->payload_len = len - i; + if (pkt->payload_len > (int)sizeof(pkt->payload)) { + LOG_WRN("checkRecv: payload too large %d", pkt->payload_len); + _mgr->free(pkt); + pkt = nullptr; + } else { + memcpy(pkt->payload, &raw[i], pkt->payload_len); + pkt->_snr = (int8_t)(_radio->getLastSNR() * 4.0f); + score = _radio->packetScore(_radio->getLastSNR(), len); + air_time = _radio->getEstAirtimeFor(len); + rx_air_time += air_time; + LOG_INF("checkRecv: header=0x%02x type=%d route=%s path_len=%d payload_len=%d", + pkt->header, pkt->getPayloadType(), + pkt->isRouteDirect() ? "direct" : "flood", + pkt->path_len, pkt->payload_len); + /* Log path hashes to identify forwarding nodes */ + if (pkt->path_len > 0) { + LOG_INF("checkRecv: path[0]=0x%02x%s%s", + pkt->path[0], + pkt->path_len > 1 ? " path[1]=0x" : "", + pkt->path_len > 1 ? "" : ""); + if (pkt->path_len > 1) { + LOG_INF(" path bytes: %02x %02x %02x %02x", + pkt->path[0], + pkt->path_len > 1 ? pkt->path[1] : 0, + pkt->path_len > 2 ? pkt->path[2] : 0, + pkt->path_len > 3 ? pkt->path[3] : 0); + } + } + } + } + } + } + + if (pkt) { +#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING) + /* Arduino-compatible packet logging - use printk to bypass log level filtering */ + { + static uint8_t packet_hash[MAX_HASH_SIZE]; + static char hash_hex[MAX_HASH_SIZE * 2 + 1]; + pkt->calculatePacketHash(packet_hash); + Utils::toHex(hash_hex, packet_hash, MAX_HASH_SIZE); + + uint8_t ptype = pkt->getPayloadType(); + if (ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ || + ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG) { + printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s [%02X -> %02X]\n", + getLogDateTime(), pkt->getRawLength(), ptype, + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + (int)pkt->getSNR(), (int)_radio->getLastRSSI(), + (int)(score * 1000), air_time, hash_hex, + (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } else { + printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s\n", + getLogDateTime(), pkt->getRawLength(), ptype, + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + (int)pkt->getSNR(), (int)_radio->getLastRSSI(), + (int)(score * 1000), air_time, hash_hex); + } + } +#endif + LOG_INF("checkRecv: processing packet"); + logRx(pkt, pkt->getRawLength(), score); + if (pkt->isRouteFlood()) { + n_recv_flood++; + int delay = calcRxDelay(score, air_time); + if (delay < 50) { + LOG_INF("checkRecv: processing flood packet immediately"); + processRecvPacket(pkt); + } else { + if (delay > (int)MAX_RX_DELAY_MILLIS) delay = MAX_RX_DELAY_MILLIS; + LOG_INF("checkRecv: queueing flood packet, delay=%d", delay); + _mgr->queueInbound(pkt, futureMillis(delay)); + } + } else { + n_recv_direct++; + LOG_INF("checkRecv: processing direct packet"); + processRecvPacket(pkt); + } + LOG_INF("checkRecv: done"); + } +} + +void Dispatcher::processRecvPacket(Packet *pkt) +{ + DispatcherAction action = onRecvPacket(pkt); + if (action == ACTION_RELEASE) { + _mgr->free(pkt); + } else if (action == ACTION_MANUAL_HOLD) { + /* subclass holds packet */ + } else { + uint8_t priority = (uint8_t)((action >> 24) - 1); + uint32_t delay = action & 0xFFFFFF; + _mgr->queueOutbound(pkt, priority, futureMillis((int)delay)); + } +} + +void Dispatcher::checkSend() +{ + uint32_t now = (uint32_t)_ms->getMillis(); + int count = _mgr->getOutboundCount(now); + if (count == 0) return; + + if (!millisHasNowPassed(next_tx_time)) { + LOG_DBG("checkSend: waiting for tx_time (count=%d)", count); + return; + } + if (_radio->isReceiving()) { + if (cad_busy_start == 0) { + cad_busy_start = now; + LOG_INF("checkSend: channel busy, starting wait"); + } + if (now - cad_busy_start > getCADFailMaxDuration()) { + _err_flags |= ERR_EVENT_CAD_TIMEOUT; + LOG_WRN("checkSend: CAD timeout exceeded"); + } else { + next_tx_time = futureMillis((int)getCADFailRetryDelay()); + return; + } + } + cad_busy_start = 0; + + outbound = _mgr->getNextOutbound(now); + if (outbound) { + /* Duty cycle enforcement — exempt admin packets */ + if (!isAdminPacket(outbound) && _duty_cycle.isExceeded(now)) { + LOG_WRN("checkSend: duty cycle exceeded (%u/%u ms), re-queuing type=%d", + _duty_cycle.window_airtime_ms, _duty_cycle.budgetMs(), + outbound->getPayloadType()); + _mgr->queueOutbound(outbound, 0, futureMillis(5000)); + outbound = nullptr; + return; + } + LOG_INF("checkSend: got outbound type=%d payload_len=%d", outbound->getPayloadType(), outbound->payload_len); + uint8_t raw[MAX_TRANS_UNIT]; + int len = 0; + raw[len++] = outbound->header; + if (outbound->hasTransportCodes()) { + memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2; + memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2; + } + raw[len++] = outbound->path_len; + memcpy(&raw[len], outbound->path, outbound->path_len); + len += outbound->path_len; + + if (len + outbound->payload_len > MAX_TRANS_UNIT) { + LOG_WRN("checkSend: packet too large len=%d+%d > %d", len, outbound->payload_len, MAX_TRANS_UNIT); + _mgr->free(outbound); + outbound = nullptr; + } else { + memcpy(&raw[len], outbound->payload, outbound->payload_len); + len += outbound->payload_len; + + uint32_t max_airtime = _radio->getEstAirtimeFor(len) * 3 / 2; + outbound_start = now; + +#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING) + /* Arduino-compatible packet logging - use printk to bypass log level filtering */ + { + uint8_t ptype = outbound->getPayloadType(); + if (ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ || + ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG) { + printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d) [%02X -> %02X]\n", + getLogDateTime(), len, ptype, + outbound->isRouteDirect() ? "D" : "F", outbound->payload_len, + (uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]); + } else { + printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d)\n", + getLogDateTime(), len, ptype, + outbound->isRouteDirect() ? "D" : "F", outbound->payload_len); + } + } +#endif + + /* Final LBT check — close the gap between initial + * isReceiving() and actual TX start (serialisation + + * logging can take 1-5 ms). */ + if (_radio->isReceiving()) { + LOG_INF("checkSend: channel busy at TX commit, re-queuing"); + _mgr->queueOutbound(outbound, 0, futureMillis((int)getCADFailRetryDelay())); + outbound = nullptr; + return; + } + + LOG_INF("checkSend: calling startSendRaw len=%d", len); + bool success = _radio->startSendRaw(raw, len); + if (!success) { + LOG_ERR("checkSend: startSendRaw failed!"); + logTxFail(outbound, outbound->getRawLength()); + releasePacket(outbound); + outbound = nullptr; + } else { + LOG_INF("checkSend: TX started, max_airtime=%u", max_airtime); + outbound_expiry = futureMillis((int)max_airtime); + } + } + } +} + +Packet *Dispatcher::obtainNewPacket() +{ + Packet *pkt = _mgr->allocNew(); + if (pkt == nullptr) { + _err_flags |= ERR_EVENT_FULL; + } else { + pkt->payload_len = pkt->path_len = 0; + pkt->_snr = 0; + } + return pkt; +} + +void Dispatcher::releasePacket(Packet *packet) +{ + _mgr->free(packet); +} + +void Dispatcher::sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis) +{ + LOG_INF("sendPacket: type=%d payload_len=%d path_len=%d pri=%d delay=%u", + packet->getPayloadType(), packet->payload_len, packet->path_len, priority, delay_millis); + if (packet->path_len > MAX_PATH_SIZE || packet->payload_len > MAX_PACKET_PAYLOAD) { + LOG_WRN("sendPacket: rejected - path_len=%d or payload_len=%d too large", + packet->path_len, packet->payload_len); + _mgr->free(packet); + } else { + _mgr->queueOutbound(packet, priority, futureMillis((int)delay_millis)); + LOG_INF("sendPacket: queued outbound count=%d", _mgr->getOutboundCount((uint32_t)_ms->getMillis())); + } +} + +bool Dispatcher::millisHasNowPassed(uint32_t timestamp) const +{ + return (int32_t)((uint32_t)_ms->getMillis() - timestamp) > 0; +} + +uint32_t Dispatcher::futureMillis(int millis_from_now) const +{ + return (uint32_t)_ms->getMillis() + millis_from_now; +} + +} /* namespace mesh */ diff --git a/zephcore/src/Identity.cpp b/zephcore/src/Identity.cpp new file mode 100644 index 0000000..9bb2a29 --- /dev/null +++ b/zephcore/src/Identity.cpp @@ -0,0 +1,130 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Identity - Ed25519 sign/verify + */ + +#include +#include +#define ED25519_NO_SEED 1 +#include + +namespace mesh { + +Identity::Identity() +{ + memset(pub_key, 0, sizeof(pub_key)); +} + +Identity::Identity(const char *pub_hex) +{ + Utils::fromHex(pub_key, PUB_KEY_SIZE, pub_hex); +} + +bool Identity::verify(const uint8_t *sig, const uint8_t *message, int msg_len) const +{ + return ed25519_verify(sig, message, (size_t)msg_len, pub_key) == 1; +} + +bool Identity::readFrom(const uint8_t *src, size_t len) +{ + if (len < PUB_KEY_SIZE) return false; + memcpy(pub_key, src, PUB_KEY_SIZE); + return true; +} + +bool Identity::writeTo(uint8_t *dest, size_t max_len) const +{ + if (max_len < PUB_KEY_SIZE) return false; + memcpy(dest, pub_key, PUB_KEY_SIZE); + return true; +} + +LocalIdentity::LocalIdentity() +{ + memset(prv_key, 0, sizeof(prv_key)); +} + +LocalIdentity::LocalIdentity(const char *prv_hex, const char *pub_hex) : Identity(pub_hex) +{ + Utils::fromHex(prv_key, PRV_KEY_SIZE, prv_hex); +} + +LocalIdentity::LocalIdentity(RNG *rng) +{ + uint8_t seed[SEED_SIZE]; + rng->random(seed, SEED_SIZE); + ed25519_create_keypair(pub_key, prv_key, seed); +} + +bool LocalIdentity::validatePrivateKey(const uint8_t prv[64]) +{ + uint8_t pub[32]; + ed25519_derive_pub(pub, prv); + if (pub[0] == 0x00 || pub[0] == 0xFF) return false; + + const uint8_t test_client_prv[64] = { + 0x70, 0x65, 0xe1, 0x8f, 0xd9, 0xfa, 0xbb, 0x70, + 0xc1, 0xed, 0x90, 0xdc, 0xa1, 0x99, 0x07, 0xde, + 0x69, 0x8c, 0x88, 0xb7, 0x09, 0xea, 0x14, 0x6e, + 0xaf, 0xd9, 0x3d, 0x9b, 0x83, 0x0c, 0x7b, 0x60, + 0xc4, 0x68, 0x11, 0x93, 0xc7, 0x9b, 0xbc, 0x39, + 0x94, 0x5b, 0xa8, 0x06, 0x41, 0x04, 0xbb, 0x61, + 0x8f, 0x8f, 0xd7, 0xa8, 0x4a, 0x0a, 0xf6, 0xf5, + 0x70, 0x33, 0xd6, 0xe8, 0xdd, 0xcd, 0x64, 0x71 + }; + const uint8_t test_client_pub[32] = { + 0x1e, 0xc7, 0x71, 0x75, 0xb0, 0x91, 0x8e, 0xd2, + 0x06, 0xf9, 0xae, 0x04, 0xec, 0x13, 0x6d, 0x6d, + 0x5d, 0x43, 0x15, 0xbb, 0x26, 0x30, 0x54, 0x27, + 0xf6, 0x45, 0xb4, 0x92, 0xe9, 0x35, 0x0c, 0x10 + }; + + uint8_t ss1[32], ss2[32]; + ed25519_key_exchange(ss1, test_client_pub, prv); + ed25519_key_exchange(ss2, pub, test_client_prv); + if (memcmp(ss1, ss2, 32) != 0) return false; + + for (int i = 0; i < 32; i++) { + if (ss1[i] != 0) return true; + } + return false; +} + +bool LocalIdentity::readFrom(const uint8_t *src, size_t len) +{ + if (len == PRV_KEY_SIZE + PUB_KEY_SIZE) { + memcpy(prv_key, src, PRV_KEY_SIZE); + memcpy(pub_key, src + PRV_KEY_SIZE, PUB_KEY_SIZE); + return true; + } + if (len == PRV_KEY_SIZE) { + memcpy(prv_key, src, PRV_KEY_SIZE); + ed25519_derive_pub(pub_key, prv_key); + return true; + } + return false; +} + +size_t LocalIdentity::writeTo(uint8_t *dest, size_t max_len) const +{ + if (max_len < PRV_KEY_SIZE) return 0; + if (max_len < PRV_KEY_SIZE + PUB_KEY_SIZE) { + memcpy(dest, prv_key, PRV_KEY_SIZE); + return PRV_KEY_SIZE; + } + memcpy(dest, prv_key, PRV_KEY_SIZE); + memcpy(dest + PRV_KEY_SIZE, pub_key, PUB_KEY_SIZE); + return PRV_KEY_SIZE + PUB_KEY_SIZE; +} + +void LocalIdentity::sign(uint8_t *sig, const uint8_t *message, int msg_len) const +{ + ed25519_sign(sig, message, (size_t)msg_len, pub_key, prv_key); +} + +void LocalIdentity::calcSharedSecret(uint8_t *secret, const uint8_t *other_pub_key) const +{ + ed25519_key_exchange(secret, other_pub_key, prv_key); +} + +} /* namespace mesh */ diff --git a/zephcore/src/Mesh.cpp b/zephcore/src/Mesh.cpp new file mode 100644 index 0000000..67e8943 --- /dev/null +++ b/zephcore/src/Mesh.cpp @@ -0,0 +1,668 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Mesh - minimal port for Phase 5 + */ + +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_mesh, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); + +namespace mesh { + +Mesh::Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables) + : Dispatcher(radio, ms, mgr), _rng(&rng), _rtc(&rtc), _tables(&tables) +{ +} + +void Mesh::begin() +{ + Dispatcher::begin(); +} + +void Mesh::loop() +{ + Dispatcher::loop(); +} + +bool Mesh::allowPacketForward(const Packet *packet) +{ + (void)packet; + return false; +} + +uint32_t Mesh::getRetransmitDelay(const Packet *packet) +{ + uint32_t t = (_radio->getEstAirtimeFor(packet->getRawLength()) * 52 / 50) / 2; + return _rng->nextInt(0, 3) * t; +} + +uint32_t Mesh::getCADFailRetryDelay() const +{ + return _rng->nextInt(1, 4) * 120; +} + +void Mesh::removeSelfFromPath(Packet *pkt) +{ + pkt->path_len -= PATH_HASH_SIZE; + for (int k = 0; k < (int)pkt->path_len; k++) { + pkt->path[k] = pkt->path[k + 1]; + } +} + +DispatcherAction Mesh::routeRecvPacket(Packet *packet) +{ + if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit() + && packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) { + packet->path_len += self_id.copyHashTo(&packet->path[packet->path_len]); + uint32_t d = getRetransmitDelay(packet); + return ACTION_RETRANSMIT_DELAYED(packet->path_len, d); + } + return ACTION_RELEASE; +} + +DispatcherAction Mesh::forwardMultipartDirect(Packet *pkt) +{ + uint8_t remaining = pkt->payload[0] >> 4; + uint8_t type = pkt->payload[0] & 0x0F; + if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { + Packet tmp; + tmp.header = pkt->header; + tmp.path_len = pkt->path_len; + memcpy(tmp.path, pkt->path, pkt->path_len); + tmp.payload_len = pkt->payload_len - 1; + memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); + if (!_tables->hasSeen(&tmp)) { + removeSelfFromPath(&tmp); + routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); + } + } + return ACTION_RELEASE; +} + +void Mesh::routeDirectRecvAcks(Packet *packet, uint32_t delay_millis) +{ + if (!packet->isMarkedDoNotRetransmit()) { + uint32_t crc; + memcpy(&crc, packet->payload, 4); + Packet *a2 = createAck(crc); + if (a2) { + memcpy(a2->path, packet->path, packet->path_len); + a2->path_len = packet->path_len; + a2->header &= ~PH_ROUTE_MASK; + a2->header |= ROUTE_TYPE_DIRECT; + sendPacket(a2, 0, delay_millis); + } + } +} + +DispatcherAction Mesh::onRecvPacket(Packet *pkt) +{ + LOG_INF("onRecvPacket: header=0x%02x type=%d route=%s path_len=%d payload[0]=0x%02x", + pkt->header, pkt->getPayloadType(), + pkt->isRouteDirect() ? "direct" : "flood", + pkt->path_len, pkt->payload_len > 0 ? pkt->payload[0] : 0); + + if (pkt->getPayloadVer() > PAYLOAD_VER_1) { + return ACTION_RELEASE; + } + + // Handle direct TRACE packets + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { + LOG_INF("TRACE packet received: path_len=%d payload_len=%d", pkt->path_len, pkt->payload_len); + if (pkt->path_len < MAX_PATH_SIZE) { + int i = 0; + uint32_t trace_tag; + memcpy(&trace_tag, &pkt->payload[i], 4); i += 4; + uint32_t auth_code; + memcpy(&auth_code, &pkt->payload[i], 4); i += 4; + uint8_t flags = pkt->payload[i++]; + uint8_t path_sz = flags & 0x03; + + uint8_t len = pkt->payload_len - i; + uint8_t offset = pkt->path_len << path_sz; + LOG_INF("TRACE: offset=%d len=%d path_sz=%d flags=0x%02x", offset, len, path_sz, flags); + if (offset >= len) { + LOG_INF("TRACE: calling onTraceRecv (reached end of path)"); + onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); + } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { + pkt->path[pkt->path_len++] = (int8_t)(pkt->getSNR() * 4); + uint32_t d = getDirectRetransmitDelay(pkt); + return ACTION_RETRANSMIT_DELAYED(5, d); + } + } + return ACTION_RELEASE; + } + + // Handle direct CONTROL packets (zero-hop only) + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) { + if (pkt->path_len == 0) { + onControlDataRecv(pkt); + } + return ACTION_RELEASE; + } + + // Handle direct zero-hop ACKs (path_len=0) + if (pkt->isRouteDirect() && pkt->path_len == 0 && pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack_crc; + memcpy(&ack_crc, pkt->payload, 4); + LOG_INF("onRecvPacket: direct zero-hop ACK ack_crc=0x%08x", ack_crc); + onAckRecv(pkt, ack_crc); + return ACTION_RELEASE; + } + + if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) { + if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack_crc; + memcpy(&ack_crc, pkt->payload, 4); + onAckRecv(pkt, ack_crc); + } + if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) { + if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { + return forwardMultipartDirect(pkt); + } + if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { + if (!_tables->hasSeen(pkt)) { + removeSelfFromPath(pkt); + routeDirectRecvAcks(pkt, 0); + } + return ACTION_RELEASE; + } + if (!_tables->hasSeen(pkt)) { + removeSelfFromPath(pkt); + return ACTION_RETRANSMIT_DELAYED(0, getDirectRetransmitDelay(pkt)); + } + } + return ACTION_RELEASE; + } + + if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE; + + DispatcherAction action = ACTION_RELEASE; + + switch (pkt->getPayloadType()) { + case PAYLOAD_TYPE_ACK: { + uint32_t ack_crc; + memcpy(&ack_crc, pkt->payload, 4); + LOG_INF("onRecvPacket: ACK packet ack_crc=0x%08x seen=%d", ack_crc, _tables->hasSeen(pkt) ? 1 : 0); + if (!_tables->hasSeen(pkt)) { + onAckRecv(pkt, ack_crc); + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_PATH: + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_TXT_MSG: { + LOG_INF("onRecvPacket: TXT_MSG/PATH/REQ/RESPONSE type=%d payload_len=%d", + pkt->getPayloadType(), pkt->payload_len); + int i = 0; + uint8_t dest_hash = pkt->payload[i++]; + uint8_t src_hash = pkt->payload[i++]; + + uint8_t *macAndData = &pkt->payload[i]; + if (i + CIPHER_MAC_SIZE >= (int)pkt->payload_len) { + LOG_WRN("onRecvPacket: incomplete packet (i=%d, payload_len=%d)", i, pkt->payload_len); + } else if (!_tables->hasSeen(pkt)) { + LOG_INF("onRecvPacket: checking dest_hash=0x%02x (self match=%d)", + dest_hash, self_id.isHashMatch(&dest_hash)); + if (self_id.isHashMatch(&dest_hash)) { + int num = searchPeersByHash(&src_hash); + LOG_INF("onRecvPacket: found %d peers matching src_hash=0x%02x", num, src_hash); + bool found = false; + for (int j = 0; j < num; j++) { + uint8_t secret[PUB_KEY_SIZE]; + getPeerSharedSecret(secret, j); + + uint8_t data[MAX_PACKET_PAYLOAD]; + int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); + LOG_INF("onRecvPacket: decrypt attempt j=%d, result len=%d", j, len); + if (len > 0) { + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { + int k = 0; + uint8_t path_len = data[k++]; + uint8_t *path = &data[k]; k += path_len; + uint8_t extra_type = data[k++] & 0x0F; + uint8_t *extra = &data[k]; + uint8_t extra_len = len - k; + if (onPeerPathRecv(pkt, j, secret, path, path_len, extra_type, extra, extra_len)) { + if (pkt->isRouteFlood()) { + Packet *rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, nullptr, 0); + if (rpath) sendDirect(rpath, path, path_len, 500); + } + } + } else { + LOG_INF("onRecvPacket: calling onPeerDataRecv type=%d len=%d", pkt->getPayloadType(), len); + onPeerDataRecv(pkt, pkt->getPayloadType(), j, secret, data, len); + } + found = true; + break; + } + } + if (found) { + pkt->markDoNotRetransmit(); + } else { + LOG_WRN("onRecvPacket: no peer could decrypt message"); + } + } else { + LOG_INF("onRecvPacket: dest_hash doesn't match self"); + } + action = routeRecvPacket(pkt); + } else { + LOG_INF("onRecvPacket: packet already seen"); + } + break; + } + case PAYLOAD_TYPE_ANON_REQ: { + int i = 0; + uint8_t dest_hash = pkt->payload[i++]; + uint8_t *sender_pub_key = &pkt->payload[i]; i += PUB_KEY_SIZE; + + uint8_t *macAndData = &pkt->payload[i]; + if (i + 2 >= (int)pkt->payload_len) { + // incomplete packet + } else if (!_tables->hasSeen(pkt)) { + if (self_id.isHashMatch(&dest_hash)) { + Identity sender(sender_pub_key); + uint8_t secret[PUB_KEY_SIZE]; + self_id.calcSharedSecret(secret, sender); + + uint8_t data[MAX_PACKET_PAYLOAD]; + int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); + if (len > 0) { + onAnonDataRecv(pkt, secret, sender, data, len); + pkt->markDoNotRetransmit(); + } + } + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_GRP_DATA: + case PAYLOAD_TYPE_GRP_TXT: { + int i = 0; + uint8_t channel_hash = pkt->payload[i++]; + + uint8_t *macAndData = &pkt->payload[i]; + if (i + 2 >= (int)pkt->payload_len) { + // incomplete packet + } else if (!_tables->hasSeen(pkt)) { + GroupChannel channels[4]; + int num = searchChannelsByHash(&channel_hash, channels, 4); + for (int j = 0; j < num; j++) { + uint8_t data[MAX_PACKET_PAYLOAD]; + int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, pkt->payload_len - i); + if (len > 0) { + onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len); + break; + } + } + action = routeRecvPacket(pkt); + } + break; + } + case PAYLOAD_TYPE_ADVERT: { + int i = 0; + Identity id; + memcpy(id.pub_key, &pkt->payload[i], PUB_KEY_SIZE); + i += PUB_KEY_SIZE; + uint32_t timestamp; + memcpy(×tamp, &pkt->payload[i], 4); + i += 4; + const uint8_t *signature = &pkt->payload[i]; + i += SIGNATURE_SIZE; + if (i <= (int)pkt->payload_len && !self_id.matches(id.pub_key) && !_tables->hasSeen(pkt)) { + uint8_t *app_data = (uint8_t *)&pkt->payload[i]; + size_t app_data_len = pkt->payload_len - (size_t)i; + if (app_data_len > MAX_ADVERT_DATA_SIZE) app_data_len = MAX_ADVERT_DATA_SIZE; + uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; + int msg_len = 0; + memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE; + memcpy(&message[msg_len], ×tamp, 4); msg_len += 4; + memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len; + if (id.verify(signature, message, msg_len)) { + onAdvertRecv(pkt, id, timestamp, app_data, app_data_len); + action = routeRecvPacket(pkt); + } + } + break; + } + case PAYLOAD_TYPE_RAW_CUSTOM: + if (pkt->isRouteDirect() && !_tables->hasSeen(pkt)) { + onRawDataRecv(pkt); + } + break; + case PAYLOAD_TYPE_MULTIPART: + if (pkt->payload_len > 2) { + /* uint8_t remaining = pkt->payload[0] >> 4; */ /* Reserved for future multipart support */ + uint8_t type = pkt->payload[0] & 0x0F; + + if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { + Packet tmp; + tmp.header = pkt->header; + tmp.path_len = pkt->path_len; + memcpy(tmp.path, pkt->path, pkt->path_len); + tmp.payload_len = pkt->payload_len - 1; + memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); + + if (!_tables->hasSeen(&tmp)) { + uint32_t ack_crc; + memcpy(&ack_crc, tmp.payload, 4); + onAckRecv(&tmp, ack_crc); + } + } + } + break; + default: + break; + } + return action; +} + +Packet *Mesh::createAdvert(const LocalIdentity &id, const uint8_t *app_data, size_t app_data_len) +{ + if (app_data_len > MAX_ADVERT_DATA_SIZE) return nullptr; + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); + int len = 0; + memcpy(&packet->payload[len], id.pub_key, PUB_KEY_SIZE); + len += PUB_KEY_SIZE; + uint32_t emitted_timestamp = _rtc->getCurrentTime(); + memcpy(&packet->payload[len], &emitted_timestamp, 4); + len += 4; + uint8_t *signature = &packet->payload[len]; + len += SIGNATURE_SIZE; + if (app_data && app_data_len > 0) { + memcpy(&packet->payload[len], app_data, app_data_len); + len += (int)app_data_len; + } + packet->payload_len = len; + + uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; + int msg_len = 0; + memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE; + memcpy(&message[msg_len], &emitted_timestamp, 4); msg_len += 4; + if (app_data && app_data_len > 0) { + memcpy(&message[msg_len], app_data, app_data_len); msg_len += (int)app_data_len; + } + id.sign(signature, message, msg_len); + return packet; +} + +Packet *Mesh::createAck(uint32_t ack_crc) +{ + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); + memcpy(packet->payload, &ack_crc, 4); + packet->payload_len = 4; + return packet; +} + +Packet *Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) +{ + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); + packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK; + memcpy(&packet->payload[1], &ack_crc, 4); + packet->payload_len = 5; + return packet; +} + +Packet *Mesh::createControlData(const uint8_t *data, size_t len) +{ + if (len > sizeof(Packet::payload)) return nullptr; + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + packet->header = (PAYLOAD_TYPE_CONTROL << PH_TYPE_SHIFT); + memcpy(packet->payload, data, len); + packet->payload_len = (uint16_t)len; + return packet; +} + +void Mesh::sendFlood(Packet *packet, uint32_t delay_millis) +{ + if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + return; + } + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_FLOOD; + packet->path_len = 0; + _tables->hasSeen(packet); + + uint8_t pri; + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 2; + } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { + pri = 3; + } else { + pri = 1; + } + sendPacket(packet, pri, delay_millis); +} + +void Mesh::sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis) +{ + if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + return; + } + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD; + packet->transport_codes[0] = transport_codes[0]; + packet->transport_codes[1] = transport_codes[1]; + packet->path_len = 0; + _tables->hasSeen(packet); + + uint8_t pri; + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 2; + } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { + pri = 3; + } else { + pri = 1; + } + sendPacket(packet, pri, delay_millis); +} + +void Mesh::sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis) +{ + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_DIRECT; + + uint8_t pri; + if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + /* For TRACE packets, path is appended to end of PAYLOAD (used for SNRs) */ + memcpy(&packet->payload[packet->payload_len], path, path_len); + packet->payload_len += path_len; + packet->path_len = 0; + pri = 5; + } else { + memcpy(packet->path, path, path_len); + packet->path_len = path_len; + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 1; + } else { + pri = 0; + } + } + + _tables->hasSeen(packet); + sendPacket(packet, pri, delay_millis); +} + +void Mesh::sendZeroHop(Packet *packet, uint32_t delay_millis) +{ + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_DIRECT; + packet->path_len = 0; + _tables->hasSeen(packet); + sendPacket(packet, 0, delay_millis); +} + +void Mesh::sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis) +{ + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_TRANSPORT_DIRECT; + packet->transport_codes[0] = transport_codes[0]; + packet->transport_codes[1] = transport_codes[1]; + packet->path_len = 0; + _tables->hasSeen(packet); + sendPacket(packet, 0, delay_millis); +} + +#define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE) + +Packet *Mesh::createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len, + uint8_t extra_type, const uint8_t *extra, size_t extra_len) +{ + uint8_t dest_hash[PATH_HASH_SIZE]; + dest.copyHashTo(dest_hash); + return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len); +} + +Packet *Mesh::createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len, + uint8_t extra_type, const uint8_t *extra, size_t extra_len) +{ + if (path_len + extra_len + 5 > MAX_COMBINED_PATH) return nullptr; + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (PAYLOAD_TYPE_PATH << PH_TYPE_SHIFT); + + int len = 0; + memcpy(&packet->payload[len], dest_hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; + len += self_id.copyHashTo(&packet->payload[len]); + + { + int data_len = 0; + uint8_t data[MAX_PACKET_PAYLOAD]; + + data[data_len++] = path_len; + memcpy(&data[data_len], path, path_len); data_len += path_len; + if (extra_len > 0) { + data[data_len++] = extra_type; + memcpy(&data[data_len], extra, extra_len); data_len += extra_len; + } else { + data[data_len++] = 0xFF; // dummy payload type + _rng->random(&data[data_len], 4); data_len += 4; + } + + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + } + + packet->payload_len = len; + return packet; +} + +Packet *Mesh::createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len) +{ + LOG_INF("createDatagram: type=%d data_len=%u", type, (unsigned)data_len); + if (type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_RESPONSE) { + if (data_len + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) { + LOG_WRN("createDatagram: data too large"); + return nullptr; + } + } else { + LOG_WRN("createDatagram: unsupported type %d", type); + return nullptr; + } + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) { + LOG_WRN("createDatagram: packet alloc failed"); + return nullptr; + } + + packet->header = (type << PH_TYPE_SHIFT); + + int len = 0; + len += dest.copyHashTo(&packet->payload[len]); + len += self_id.copyHashTo(&packet->payload[len]); + LOG_INF("createDatagram: dest_hash=0x%02x src_hash=0x%02x", packet->payload[0], packet->payload[1]); + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + + packet->payload_len = len; + LOG_INF("createDatagram: created packet payload_len=%d", len); + return packet; +} + +Packet *Mesh::createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest, + const uint8_t *secret, const uint8_t *data, size_t data_len) +{ + if (type == PAYLOAD_TYPE_ANON_REQ) { + if (data_len + 1 + PUB_KEY_SIZE + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) return nullptr; + } else { + return nullptr; + } + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (type << PH_TYPE_SHIFT); + + int len = 0; + if (type == PAYLOAD_TYPE_ANON_REQ) { + len += dest.copyHashTo(&packet->payload[len]); + memcpy(&packet->payload[len], sender.pub_key, PUB_KEY_SIZE); len += PUB_KEY_SIZE; + } + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + + packet->payload_len = len; + return packet; +} + +Packet *Mesh::createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len) +{ + if (!(type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA)) return nullptr; + if (data_len + 1 + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) return nullptr; + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (type << PH_TYPE_SHIFT); + + int len = 0; + memcpy(&packet->payload[len], channel.hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; + len += Utils::encryptThenMAC(channel.secret, &packet->payload[len], data, data_len); + + packet->payload_len = len; + return packet; +} + +Packet *Mesh::createRawData(const uint8_t *data, size_t len) +{ + if (len > sizeof(Packet::payload)) return nullptr; + + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT); + memcpy(packet->payload, data, len); + packet->payload_len = (uint16_t)len; + + return packet; +} + +Packet *Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags) +{ + Packet *packet = obtainNewPacket(); + if (packet == nullptr) return nullptr; + + packet->header = (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT); + memcpy(packet->payload, &tag, 4); + memcpy(&packet->payload[4], &auth_code, 4); + packet->payload[8] = flags; + packet->payload_len = 9; + + return packet; +} + +} /* namespace mesh */ diff --git a/zephcore/src/Packet.cpp b/zephcore/src/Packet.cpp new file mode 100644 index 0000000..e73e2b7 --- /dev/null +++ b/zephcore/src/Packet.cpp @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Packet implementation + */ + +#include +#include +#include + +namespace mesh { + +Packet::Packet() +{ + header = 0; + path_len = 0; + payload_len = 0; +} + +int Packet::getRawLength() const +{ + return 2 + path_len + payload_len + (hasTransportCodes() ? 4 : 0); +} + +void Packet::calculatePacketHash(uint8_t *hash) const +{ + uint8_t t = getPayloadType(); + if (t == PAYLOAD_TYPE_TRACE) { + uint8_t buf[2 + MAX_PACKET_PAYLOAD]; + buf[0] = t; + memcpy(buf + 1, &path_len, sizeof(path_len)); + memcpy(buf + 2, payload, payload_len); + Utils::sha256(hash, MAX_HASH_SIZE, buf, 2 + payload_len); + } else { + uint8_t buf[1 + MAX_PACKET_PAYLOAD]; + buf[0] = t; + memcpy(buf + 1, payload, payload_len); + Utils::sha256(hash, MAX_HASH_SIZE, buf, 1 + payload_len); + } +} + +uint8_t Packet::writeTo(uint8_t dest[]) const +{ + uint8_t i = 0; + dest[i++] = header; + if (hasTransportCodes()) { + memcpy(&dest[i], &transport_codes[0], 2); i += 2; + memcpy(&dest[i], &transport_codes[1], 2); i += 2; + } + dest[i++] = path_len; + memcpy(&dest[i], path, path_len); i += path_len; + memcpy(&dest[i], payload, payload_len); i += payload_len; + return i; +} + +bool Packet::readFrom(const uint8_t src[], uint8_t len) +{ + uint8_t i = 0; + header = src[i++]; + if (hasTransportCodes()) { + memcpy(&transport_codes[0], &src[i], 2); i += 2; + memcpy(&transport_codes[1], &src[i], 2); i += 2; + } else { + transport_codes[0] = transport_codes[1] = 0; + } + path_len = src[i++]; + if (path_len > sizeof(path)) return false; + memcpy(path, &src[i], path_len); i += path_len; + if (i >= len) return false; + payload_len = len - i; + if (payload_len > sizeof(payload)) return false; + memcpy(payload, &src[i], payload_len); + return true; +} + +} /* namespace mesh */ diff --git a/zephcore/src/StaticPoolPacketManager.cpp b/zephcore/src/StaticPoolPacketManager.cpp new file mode 100644 index 0000000..6bbd4f3 --- /dev/null +++ b/zephcore/src/StaticPoolPacketManager.cpp @@ -0,0 +1,145 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Static pool PacketManager - no dynamic allocation + */ + +#include +#include +#include + +namespace mesh { + +#define POOL_SIZE 16 +#define QUEUE_SIZE 16 + +struct PacketQueue { + Packet *_table[QUEUE_SIZE]; + uint8_t _pri_table[QUEUE_SIZE]; + uint32_t _schedule_table[QUEUE_SIZE]; + int _num; + + PacketQueue() : _num(0) { + memset(_table, 0, sizeof(_table)); + } + + int countBefore(uint32_t now) const { + int n = 0; + for (int j = 0; j < _num; j++) { + if (_schedule_table[j] > now) continue; + n++; + } + return n; + } + + Packet *get(uint32_t now) { + uint8_t min_pri = 0xFF; + int best_idx = -1; + for (int j = 0; j < _num; j++) { + if (_schedule_table[j] > now) continue; + if (_pri_table[j] < min_pri) { + min_pri = _pri_table[j]; + best_idx = j; + } + } + if (best_idx < 0) return nullptr; + + Packet *top = _table[best_idx]; + for (int i = best_idx; i < _num - 1; i++) { + _table[i] = _table[i + 1]; + _pri_table[i] = _pri_table[i + 1]; + _schedule_table[i] = _schedule_table[i + 1]; + } + _num--; + return top; + } + + Packet *removeByIdx(int i) { + if (i >= _num) return nullptr; + Packet *item = _table[i]; + for (int j = i; j < _num - 1; j++) { + _table[j] = _table[j + 1]; + _pri_table[j] = _pri_table[j + 1]; + _schedule_table[j] = _schedule_table[j + 1]; + } + _num--; + return item; + } + + void add(Packet *packet, uint8_t priority, uint32_t scheduled_for) { + if (_num >= QUEUE_SIZE) return; + _table[_num] = packet; + _pri_table[_num] = priority; + _schedule_table[_num] = scheduled_for; + _num++; + } + + int count() const { return _num; } + Packet *itemAt(int i) const { return (i < _num) ? _table[i] : nullptr; } +}; + +static Packet _packet_pool[POOL_SIZE]; +static PacketQueue _unused; +static PacketQueue _send_queue; +static PacketQueue _rx_queue; +static bool _initialized = false; + +static void init_pool() { + if (_initialized) return; + _initialized = true; + for (int i = 0; i < POOL_SIZE; i++) { + _unused.add(&_packet_pool[i], 0, 0); + } +} + +Packet *StaticPoolPacketManager::allocNew() +{ + init_pool(); + return _unused.removeByIdx(0); +} + +void StaticPoolPacketManager::free(Packet *packet) +{ + if (packet) _unused.add(packet, 0, 0); +} + +void StaticPoolPacketManager::queueOutbound(Packet *packet, uint8_t priority, uint32_t scheduled_for) +{ + _send_queue.add(packet, priority, scheduled_for); +} + +Packet *StaticPoolPacketManager::getNextOutbound(uint32_t now) +{ + return _send_queue.get(now); +} + +int StaticPoolPacketManager::getOutboundCount(uint32_t now) const +{ + return _send_queue.countBefore(now); +} + +int StaticPoolPacketManager::getFreeCount() const +{ + return _unused.count(); +} + +Packet *StaticPoolPacketManager::getOutboundByIdx(int i) +{ + return _send_queue.itemAt(i); +} + +Packet *StaticPoolPacketManager::removeOutboundByIdx(int i) +{ + return _send_queue.removeByIdx(i); +} + +void StaticPoolPacketManager::queueInbound(Packet *packet, uint32_t scheduled_for) +{ + _rx_queue.add(packet, 0, scheduled_for); +} + +Packet *StaticPoolPacketManager::getNextInbound(uint32_t now) +{ + return _rx_queue.get(now); +} + +} /* namespace mesh */ diff --git a/zephcore/src/Utils.cpp b/zephcore/src/Utils.cpp new file mode 100644 index 0000000..8e66d7b --- /dev/null +++ b/zephcore/src/Utils.cpp @@ -0,0 +1,226 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Utils - mbedTLS backend for AES-ECB, SHA256, HMAC + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_utils, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); + +namespace mesh { + +static uint8_t hexVal(char c) +{ + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= '0' && c <= '9') return c - '0'; + return 0; +} + +void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t *msg, int msg_len) +{ + uint8_t full_hash[32]; + mbedtls_sha256(msg, (size_t)msg_len, full_hash, 0); + size_t copy_len = (hash_len < 32) ? hash_len : 32; + memcpy(hash, full_hash, copy_len); +} + +void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t *frag1, int frag1_len, const uint8_t *frag2, int frag2_len) +{ + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); + mbedtls_sha256_update(&ctx, frag1, (size_t)frag1_len); + mbedtls_sha256_update(&ctx, frag2, (size_t)frag2_len); + uint8_t full_hash[32]; + mbedtls_sha256_finish(&ctx, full_hash); + mbedtls_sha256_free(&ctx); + size_t copy_len = (hash_len < 32) ? hash_len : 32; + memcpy(hash, full_hash, copy_len); +} + +/* AES-ECB using mbedTLS low-level API (PSA doesn't support ECB mode) */ +static int aes_ecb_encrypt(const uint8_t *key, size_t key_len, const uint8_t *src, int src_len, uint8_t *dest) +{ + if (src_len % 16 != 0) { + LOG_ERR("src_len=%d not multiple of 16", src_len); + return -1; + } + + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + + int ret = mbedtls_aes_setkey_enc(&ctx, key, key_len * 8); + if (ret != 0) { + LOG_ERR("setkey failed: %d", ret); + mbedtls_aes_free(&ctx); + return -1; + } + + /* Encrypt each 16-byte block */ + for (int i = 0; i < src_len; i += 16) { + ret = mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, src + i, dest + i); + if (ret != 0) { + LOG_ERR("crypt_ecb failed at block %d: %d", i/16, ret); + mbedtls_aes_free(&ctx); + return -1; + } + } + + mbedtls_aes_free(&ctx); + return src_len; +} + +static int aes_ecb_decrypt(const uint8_t *key, size_t key_len, const uint8_t *src, int src_len, uint8_t *dest) +{ + if (src_len % 16 != 0) { + LOG_ERR("src_len=%d not multiple of 16", src_len); + return -1; + } + + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + + int ret = mbedtls_aes_setkey_dec(&ctx, key, key_len * 8); + if (ret != 0) { + LOG_ERR("setkey failed: %d", ret); + mbedtls_aes_free(&ctx); + return -1; + } + + /* Decrypt each 16-byte block */ + for (int i = 0; i < src_len; i += 16) { + ret = mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_DECRYPT, src + i, dest + i); + if (ret != 0) { + LOG_ERR("crypt_ecb failed at block %d: %d", i/16, ret); + mbedtls_aes_free(&ctx); + return -1; + } + } + + mbedtls_aes_free(&ctx); + return src_len; +} + +int Utils::encrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len) +{ + uint8_t tmp[MAX_PACKET_PAYLOAD + CIPHER_BLOCK_SIZE]; + const uint8_t *sp = src; + uint8_t *dp = tmp; + int rem = src_len; + while (rem >= (int)CIPHER_BLOCK_SIZE) { + memcpy(dp, sp, CIPHER_BLOCK_SIZE); + dp += CIPHER_BLOCK_SIZE; + sp += CIPHER_BLOCK_SIZE; + rem -= CIPHER_BLOCK_SIZE; + } + if (rem > 0) { + memset(dp, 0, CIPHER_BLOCK_SIZE); + memcpy(dp, sp, (size_t)rem); + dp += CIPHER_BLOCK_SIZE; + } + int total = (int)(dp - tmp); + int n = aes_ecb_encrypt(shared_secret, CIPHER_KEY_SIZE, tmp, total, dest); + return (n > 0) ? n : 0; +} + +int Utils::decrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len) +{ + int n = aes_ecb_decrypt(shared_secret, CIPHER_KEY_SIZE, src, src_len, dest); + return (n > 0) ? n : 0; +} + +/* HMAC-SHA256 using mbedTLS */ +static int compute_hmac_truncated(const uint8_t *key, size_t key_len, const uint8_t *data, size_t data_len, uint8_t *mac_out, size_t mac_len) +{ + uint8_t full_hmac[32]; + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + if (md_info == nullptr) { + LOG_ERR("compute_hmac: SHA256 not available"); + return -1; + } + + int ret = mbedtls_md_hmac(md_info, key, key_len, data, data_len, full_hmac); + if (ret != 0) { + LOG_ERR("compute_hmac: mbedtls_md_hmac failed: %d", ret); + return -1; + } + + memcpy(mac_out, full_hmac, mac_len); + return 0; +} + +int Utils::encryptThenMAC(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len) +{ + int enc_len = encrypt(shared_secret, dest + CIPHER_MAC_SIZE, src, src_len); + if (enc_len <= 0) return 0; + if (compute_hmac_truncated(shared_secret, PUB_KEY_SIZE, dest + CIPHER_MAC_SIZE, (size_t)enc_len, dest, CIPHER_MAC_SIZE) != 0) + return 0; + return CIPHER_MAC_SIZE + enc_len; +} + +int Utils::MACThenDecrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len) +{ + if (src_len <= (int)CIPHER_MAC_SIZE) return 0; + uint8_t computed_mac[CIPHER_MAC_SIZE]; + if (compute_hmac_truncated(shared_secret, PUB_KEY_SIZE, src + CIPHER_MAC_SIZE, (size_t)src_len - CIPHER_MAC_SIZE, computed_mac, CIPHER_MAC_SIZE) != 0) + return 0; + if (memcmp(computed_mac, src, CIPHER_MAC_SIZE) != 0) return 0; + return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); +} + +static const char hex_chars[] = "0123456789ABCDEF"; + +void Utils::toHex(char *dest, const uint8_t *src, size_t len) +{ + while (len > 0) { + uint8_t b = *src++; + *dest++ = hex_chars[b >> 4]; + *dest++ = hex_chars[b & 0x0F]; + len--; + } + *dest = 0; +} + +bool Utils::fromHex(uint8_t *dest, int dest_size, const char *src_hex) +{ + size_t len = strlen(src_hex); + if (len != (size_t)(dest_size * 2)) return false; + uint8_t *dp = dest; + while ((size_t)(dp - dest) < (size_t)dest_size) { + char ch = *src_hex++; + char cl = *src_hex++; + *dp++ = (uint8_t)((hexVal(ch) << 4) | hexVal(cl)); + } + return true; +} + +bool Utils::isHexChar(char c) +{ + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); +} + +int Utils::parseTextParts(char *text, const char *parts[], int max_num, char separator) +{ + int num = 0; + char *sp = text; + while (*sp && num < max_num) { + parts[num++] = sp; + while (*sp && *sp != separator) sp++; + if (*sp) { + *sp++ = 0; + } + } + while (*sp && *sp != separator) sp++; + if (*sp) *sp = 0; + return num; +} + +} /* namespace mesh */ diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp new file mode 100644 index 0000000..51d604c --- /dev/null +++ b/zephcore/src/main_companion.cpp @@ -0,0 +1,643 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore - CompanionMesh (Event-Driven) + * + * BLE stack is in adapters/ble/ZephyrBLE.cpp. + * USB CDC transport is in adapters/usb/ZephyrUsbCompanion.cpp. + * UI ↔ mesh actions are in helpers/ui/ui_mesh_actions.cpp. + * This file handles: mesh event loop, LoRa, GPS, data storage, init. + */ + +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include "ui_task.h" +#include "ui_mesh_actions.h" +#include "oled_power.h" +#if IS_ENABLED(CONFIG_ZEPHCORE_UI_BUZZER) +#include "buzzer.h" +#endif + +#include + +#if IS_ENABLED(CONFIG_LOG) +#include +#endif + +/* Radio + mesh includes (shared header selects LR1110 or SX126x) */ +#include +#ifdef ZEPHCORE_LORA +#include +#endif + +/* + * BLE Controller assert handler — without this, a controller assert + * silently freezes the CPU at highest IRQ priority with zero log output. + * This prints the file/line and reboots so we can actually see what happened. + */ +#if IS_ENABLED(CONFIG_BT_CTLR_ASSERT_HANDLER) +#include +extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) +{ + LOG_ERR("!!! BLE CONTROLLER ASSERT: %s:%u !!!", file ? file : "?", line); + /* Force flush logs before reboot */ + k_sleep(K_MSEC(100)); + sys_reboot(SYS_REBOOT_COLD); +} +#endif + +/* + * Event-driven mesh loop - replaces 50ms polling with true event signaling. + * Events are signaled from ISR/callbacks, mesh loop wakes immediately. + * NO periodic timer - CPU sleeps until actual events occur. + */ +#define MESH_EVENT_LORA_RX BIT(0) /* LoRa packet received */ +#define MESH_EVENT_LORA_TX_DONE BIT(1) /* LoRa TX complete (event-driven!) */ +#define MESH_EVENT_BLE_RX BIT(2) /* BLE frame received */ +#define MESH_EVENT_HOUSEKEEPING BIT(3) /* Periodic housekeeping (noise floor, etc.) */ +#define MESH_EVENT_UI_ACTION BIT(4) /* Button action from UI (deferred to mesh thread) */ +#define MESH_EVENT_GPS_ACTION BIT(5) /* GPS state change (must run on main thread!) */ +#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_BLE_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_UI_ACTION | MESH_EVENT_GPS_ACTION) + +/* Housekeeping interval - infrequent to preserve power savings */ +#define HOUSEKEEPING_INTERVAL_MS CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS + +/* Event-driven mesh loop - k_event for signaling from ISR/callbacks */ +static struct k_event mesh_events; + +/* Work items for event-driven processing */ +static void rx_process_work_fn(struct k_work *work); +static void contact_iter_work_fn(struct k_work *work); +static void housekeeping_timer_fn(struct k_timer *timer); + +K_WORK_DEFINE(rx_process_work, rx_process_work_fn); +K_WORK_DEFINE(contact_iter_work, contact_iter_work_fn); + +/* Housekeeping timer for periodic tasks (noise floor calibration, etc.) + * Fires every 5 seconds to wake event loop for maintenance without + * compromising event-driven power savings. */ +K_TIMER_DEFINE(housekeeping_timer, housekeeping_timer_fn, NULL); + +/* Forward declarations */ +#ifdef ZEPHCORE_LORA +static CompanionMesh *companion_mesh_ptr; +#endif + +/* ========== BLE callbacks → main ========== */ + +/* BLE RX callback — called from BLE adapter when a frame arrives. + * Queues to recv_queue and wakes mesh event loop. */ +static void ble_on_rx_frame(const uint8_t *data, uint16_t len) +{ + struct { + uint16_t len; + uint8_t buf[MAX_FRAME_SIZE]; + } f; + + if (len == 0 || len > MAX_FRAME_SIZE) { + return; + } + + f.len = len; + memcpy(f.buf, data, len); + + if (k_msgq_put(zephcore_ble_get_recv_queue(), &f, K_NO_WAIT) != 0) { + LOG_WRN("recv queue full"); + return; + } + + /* Trigger RX processing and signal mesh event */ + k_work_submit(&rx_process_work); + k_event_post(&mesh_events, MESH_EVENT_BLE_RX); +} + +/* BLE TX idle callback — called when TX queue is empty. + * Continues contact iteration. */ +static void ble_on_tx_idle(void) +{ + k_work_submit(&contact_iter_work); +} + +/* BLE connected callback — notify UI, clear USB state if needed */ +static void ble_on_connected(void) +{ + ui_notify(UI_EVENT_BLE_CONNECTED); +} + +/* BLE disconnected callback — notify UI */ +static void ble_on_disconnected(void) +{ +#if IS_ENABLED(CONFIG_LOG) + zephcore_usb_companion_reset_rx(); +#endif + ui_notify(UI_EVENT_BLE_DISCONNECTED); +} + +static const struct ble_callbacks ble_cbs = { + .on_rx_frame = ble_on_rx_frame, + .on_tx_idle = ble_on_tx_idle, + .on_connected = ble_on_connected, + .on_disconnected = ble_on_disconnected, +}; + +/* ========== Frame send/receive (mesh ↔ BLE/USB) ========== */ + +static size_t write_frame(const uint8_t *src, size_t len) +{ + return zephcore_ble_send(src, (uint16_t)len); +} + +/** + * Push notification callback - sends push frames to BLE. + * Push frames have format: [push_code] [data...] + */ +static void push_callback(uint8_t code, const uint8_t *data, size_t len) +{ + /* Push frame: code byte + optional data */ + uint8_t push_buf[1 + MAX_FRAME_SIZE - 1]; + size_t total_len = 1 + len; + + if (total_len > MAX_FRAME_SIZE) { + LOG_WRN("data too long %u", (unsigned)len); + total_len = MAX_FRAME_SIZE; + len = MAX_FRAME_SIZE - 1; + } + + push_buf[0] = code; + if (len > 0 && data != nullptr) { + memcpy(&push_buf[1], data, len); + } + + LOG_INF("code=0x%02x len=%u (total frame)", code, (unsigned)total_len); + write_frame(push_buf, total_len); +} + +/* RX processing work - handles received BLE/USB frames */ +static void rx_process_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + struct { + uint16_t len; + uint8_t buf[MAX_FRAME_SIZE]; + } f; + + /* Process all queued frames */ + while (k_msgq_get(zephcore_ble_get_recv_queue(), &f, K_NO_WAIT) == 0) { +#ifdef ZEPHCORE_LORA + /* Reset contact iterator when new command received */ + companion_mesh_ptr->resetContactIterator(); + if (!companion_mesh_ptr->handleProtocolFrame(f.buf, f.len)) { + LOG_DBG("rx_process: unknown cmd 0x%02x len=%u", f.buf[0], (unsigned)f.len); + uint8_t err_rsp[] = { 0x01, 0x01 }; /* PACKET_ERROR, ERR_UNSUPPORTED */ + write_frame(err_rsp, sizeof(err_rsp)); + } +#endif + } +} + +/* Contact iteration work - runs when TX queue has space */ +static void contact_iter_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); +#ifdef ZEPHCORE_LORA + /* Check if send queue has space before continuing iteration + * Match Arduino's isWriteBusy(): busy when queue >= 2/3 full (8 of 12) + */ + uint32_t used = k_msgq_num_used_get(zephcore_ble_get_send_queue()); + uint32_t queue_size = CONFIG_ZEPHCORE_BLE_QUEUE_SIZE; + bool has_space = (used < (queue_size * 2 / 3)); + + if (has_space && companion_mesh_ptr) { + if (companion_mesh_ptr->continueContactIteration()) { + /* More contacts to send - kick TX drain which will re-trigger us */ + zephcore_ble_kick_tx(); + } + } +#endif +} + +/* + * Event-driven mesh loop - runs in main thread context. + * Wakes on actual events: LoRa RX, LoRa TX done, BLE RX. + * Plus a 5-second housekeeping timer for noise floor calibration, etc. + */ +static void mesh_event_loop(void) +{ + LOG_INF("starting event-driven loop"); + + /* Start housekeeping timer for periodic maintenance tasks */ + k_timer_start(&housekeeping_timer, K_MSEC(HOUSEKEEPING_INTERVAL_MS), + K_MSEC(HOUSEKEEPING_INTERVAL_MS)); + + for (;;) { + /* Wait for any mesh event - blocks until signaled */ + uint32_t events = k_event_wait(&mesh_events, MESH_EVENT_ALL, false, K_FOREVER); + + /* Clear the events we're handling */ + k_event_clear(&mesh_events, events); + +#ifdef ZEPHCORE_LORA + /* Handle deferred UI button actions (flood advert, pref saves). + * Must run in this thread — LoRa TX and flash writes block. */ + if (events & MESH_EVENT_UI_ACTION) { + mesh_handle_ui_actions(); + } + + /* Handle deferred GPS state transitions (wake/timeout). + * MUST run here (main thread) because GNSS configuration uses + * modem_chat_run_script() which blocks on a semaphore signaled + * from the system work queue. Running it on the system work + * queue would deadlock. */ + if (events & MESH_EVENT_GPS_ACTION) { + gps_process_event(); + } + + /* Run mesh loop - handles all pending work: + * - Process received LoRa packets + * - Check TX completion + * - Timeout handling + * - Noise floor calibration (runs when loop is called) + */ + if (companion_mesh_ptr) { + companion_mesh_ptr->loop(); + } + + /* Periodic UI refresh (every housekeeping cycle = 5s) */ + if (events & MESH_EVENT_HOUSEKEEPING) { + mesh_housekeeping_ui_refresh(); + } +#endif + } +} + +/* Housekeeping timer callback - signals event to wake mesh loop periodically */ +static void housekeeping_timer_fn(struct k_timer *timer) +{ + ARG_UNUSED(timer); + k_event_post(&mesh_events, MESH_EVENT_HOUSEKEEPING); +} + +#ifdef ZEPHCORE_LORA +/* LoRa RX callback - called from ISR context when packet received */ +static void lora_rx_callback(void *user_data) +{ + ARG_UNUSED(user_data); + /* Signal event to wake mesh loop immediately */ + k_event_post(&mesh_events, MESH_EVENT_LORA_RX); +} + +/* LoRa TX complete callback - called from work queue when async TX finishes */ +static void lora_tx_done_callback(void *user_data) +{ + ARG_UNUSED(user_data); + /* Signal event to wake mesh loop for TX completion handling */ + k_event_post(&mesh_events, MESH_EVENT_LORA_TX_DONE); +} +#endif + +static mesh::ZephyrRTCClock rtc_clock; +static ZephyrDataStore data_store(rtc_clock); + +#ifdef ZEPHCORE_LORA +static mesh::ZephyrBoard zephyr_board; + +/* NodePrefs placeholder - will be set from CompanionMesh */ +static NodePrefs temp_prefs; + +#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) +/* LR1110 via Zephyr LoRa driver */ +static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); +static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board, &temp_prefs); +#else +/* SX126x via Zephyr LoRa driver */ +static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); +static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board, &temp_prefs); +#endif + +static uint16_t get_battery_mv(void) +{ + return zephyr_board.getBattMilliVolts(); +} + +static void radio_reconfigure(void) +{ + lora_radio.reconfigure(); +} + +static mesh::ZephyrMillisecondClock ms_clock; +static mesh::ZephyrRNG zephyr_rng; +static mesh::SimpleMeshTables mesh_tables; +static mesh::StaticPoolPacketManager packet_mgr; +static CompanionMesh companion_mesh(lora_radio, ms_clock, zephyr_rng, rtc_clock, + packet_mgr, mesh_tables, data_store); +#endif + +/* GPS enable callback - logs state changes + * Note: GPS persistence is handled in CompanionMesh.cpp CMD_SET_CUSTOM_VAR handler */ +static void gps_enable_callback(bool enabled) +{ + LOG_INF("GPS %s", enabled ? "enabled" : "disabled"); + ui_set_gps_enabled(enabled); +} + +/* GPS event callback - called when GPS work handlers need the main thread + * to process a state transition (wake from standby, timeout, etc.). + * Runs from system work queue context — just posts an event, no blocking. */ +static void gps_event_callback(void) +{ + k_event_post(&mesh_events, MESH_EVENT_GPS_ACTION); +} + +/* GPS fix callback - called when GPS acquires a valid fix. + * Updates mesh node position and RTC. */ +static void gps_fix_callback(double lat, double lon, int64_t utc_time) +{ + /* Sync RTC from GPS time */ + if (utc_time > 0) { + LOG_INF("GPS fix: RTC sync time=%lld", utc_time); + rtc_clock.setCurrentTime((uint32_t)utc_time); + } + +#ifdef ZEPHCORE_LORA + /* Update node position for mesh advertising */ + if (lat != companion_mesh.prefs.node_lat || lon != companion_mesh.prefs.node_lon) { + companion_mesh.prefs.node_lat = lat; + companion_mesh.prefs.node_lon = lon; + /* Log as integer degrees (Zephyr LOG doesn't support %f by default) */ + int lat_deg = (int)lat; + int lon_deg = (int)lon; + int lat_frac = (int)((lat - lat_deg) * 1000000); + int lon_frac = (int)((lon - lon_deg) * 1000000); + if (lat_frac < 0) lat_frac = -lat_frac; + if (lon_frac < 0) lon_frac = -lon_frac; + LOG_INF("GPS fix: position updated lat=%d.%06d lon=%d.%06d", + lat_deg, lat_frac, lon_deg, lon_frac); + /* Save to storage */ + data_store.savePrefs(companion_mesh.prefs); + } + + /* Update UI with GPS data */ + { + struct gps_position gpos; + + gps_get_position(&gpos); + int32_t lat_mdeg = (int32_t)(lat * 1000.0); + int32_t lon_mdeg = (int32_t)(lon * 1000.0); + + ui_set_gps_data(true, (uint8_t)gpos.satellites, + lat_mdeg, lon_mdeg, gpos.altitude_mm); + } +#else + ARG_UNUSED(lat); + ARG_UNUSED(lon); +#endif +} + +/* bt_ready callback — BLE stack is up, start advertising */ +static void bt_ready(int err) +{ + if (err) { + LOG_ERR("bt_enable failed: %d", err); + return; + } + +#ifdef ZEPHCORE_LORA + zephcore_ble_start(companion_mesh.getDeviceName()); +#else + zephcore_ble_start(NULL); +#endif +} + +int main(void) +{ + /* Clear any stale bootloader magic from previous sessions */ + zephyr_board.clearBootloaderMagic(); + + /* Brief settle — deferred logging will catch up once USB CDC enumerates */ + k_sleep(K_MSEC(100)); + LOG_INF("=== ZephCore starting ==="); + + if (!ZephyrDataStore::mount()) { + LOG_ERR("LittleFS mount failed"); + } + data_store.begin(); + + /* Initialize sensor manager (GPS, environment sensors) */ + sensor_manager_init(); + + /* Initialize UI subsystem (buttons, buzzer, display). + * If display is present, ui_init() handles display init + auto-off. + * If no display, fall back to raw OLED sleep for power saving. */ + ui_init(); +#if !IS_ENABLED(CONFIG_ZEPHCORE_UI_DISPLAY) + oled_sleep(); +#endif + + /* Register GPS callbacks (GPS state restored from prefs later) */ + if (gps_is_available()) { + LOG_INF("GPS available"); + gps_set_enable_callback(gps_enable_callback); + gps_set_fix_callback(gps_fix_callback); + gps_set_event_callback(gps_event_callback); + } + + /* Log environment sensor availability */ + if (env_sensors_available()) { + LOG_INF("Environment sensors available"); + } + +#ifdef ZEPHCORE_LORA + /* Initialize prefs with defaults */ + memset(&companion_mesh.prefs, 0, sizeof(companion_mesh.prefs)); + companion_mesh.prefs.freq = 869.618f; + companion_mesh.prefs.bw = 62.5f; + companion_mesh.prefs.sf = 8; + companion_mesh.prefs.cr = 8; + companion_mesh.prefs.tx_power_dbm = 22; + companion_mesh.prefs.rx_delay_base = 0.0f; /* Disabled for companion */ + companion_mesh.prefs.airtime_factor = 10.0f; /* 10% duty cycle (EU 868 default) */ + + /* Generate default node name from hardware device ID */ + uint8_t dev_id[8]; + ssize_t id_len = hwinfo_get_device_id(dev_id, sizeof(dev_id)); + if (id_len >= 4) { + snprintf(companion_mesh.prefs.node_name, sizeof(companion_mesh.prefs.node_name), + "MeshCore-%02X%02X%02X%02X", dev_id[0], dev_id[1], dev_id[2], dev_id[3]); + } else { + strncpy(companion_mesh.prefs.node_name, "MeshCore", sizeof(companion_mesh.prefs.node_name) - 1); + } + + /* Load prefs from storage */ + data_store.loadPrefs(companion_mesh.prefs); + + /* Apply saved BLE PIN (0 = use Kconfig default) */ + if (companion_mesh.prefs.ble_pin >= 100000 && companion_mesh.prefs.ble_pin <= 999999) { + zephcore_ble_set_passkey(companion_mesh.prefs.ble_pin); + LOG_INF("BLE passkey loaded from prefs: %06u", companion_mesh.prefs.ble_pin); + } + + /* Copy prefs to temp_prefs for radio (radio was initialized before companion_mesh) */ + temp_prefs = companion_mesh.prefs; + + /* Load contacts and channels */ + data_store.loadContacts(&companion_mesh); + data_store.loadChannels(&companion_mesh); + + /* Add Public channel if no channels loaded (first boot) */ + if (companion_mesh.getNumChannels() == 0) { + // Public channel PSK: "izOH6cXN6mrJ5e26oRXNcg==" decoded + static const uint8_t public_psk[16] = { + 0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a, + 0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72 + }; + companion_mesh.addChannel("Public", public_psk, 16); + data_store.saveChannels(&companion_mesh); + LOG_INF("Added default Public channel"); + } + + /* Load or generate identity */ + mesh::LocalIdentity self_identity; + if (!data_store.loadMainIdentity(self_identity)) { + self_identity = mesh::LocalIdentity(&zephyr_rng); + /* Ensure pub_key[0] is not reserved (0x00 or 0xFF in MeshCore protocol) */ + int count = 0; + while (count < 10 && (self_identity.pub_key[0] == 0x00 || self_identity.pub_key[0] == 0xFF)) { + self_identity = mesh::LocalIdentity(&zephyr_rng); + count++; + } + data_store.saveMainIdentity(self_identity); + } + companion_mesh.self_id = self_identity; + + /* Set callbacks */ + companion_mesh.setWriteFrameCallback(write_frame); + companion_mesh.setPushCallback(push_callback); + companion_mesh.setBatteryCallback(get_battery_mv); + companion_mesh.setRadioReconfigureCallback(radio_reconfigure); + companion_mesh.setPinChangeCallback([](uint32_t new_pin) { + zephcore_ble_set_passkey(new_pin); + }); + companion_mesh_ptr = &companion_mesh; + + /* Set LoRa callbacks for event-driven packet processing */ + lora_radio.setRxCallback(lora_rx_callback, nullptr); + lora_radio.setTxDoneCallback(lora_tx_done_callback, nullptr); + + /* Start mesh */ + companion_mesh.begin(); + + /* Push initial state to UI display */ + ui_set_node_name(companion_mesh.prefs.node_name); + ui_set_radio_params( + (uint32_t)(companion_mesh.prefs.freq * 1000000.0f + 0.5f), + companion_mesh.prefs.sf, + (uint16_t)(companion_mesh.prefs.bw * 10.0f + 0.5f), + companion_mesh.prefs.cr, + companion_mesh.prefs.tx_power_dbm, + lora_radio.getNoiseFloor()); + ui_set_battery(zephyr_board.getBattMilliVolts(), 0); + ui_set_gps_available(gps_is_available()); + ui_set_gps_enabled(companion_mesh.prefs.gps_enabled != 0); + ui_set_ble_enabled(true); /* BLE starts advertising at boot */ + + /* Restore offgrid mode (client repeat) state from persisted prefs */ + ui_set_offgrid_mode(companion_mesh.prefs.client_repeat != 0); + LOG_INF("offgrid mode: %s (from prefs)", + companion_mesh.prefs.client_repeat ? "on" : "off"); + + /* Restore buzzer mute state from persisted prefs, then play + * startup chime only if buzzer is not muted. + * buzzer_init() defaults to quiet=true, so we must always + * call buzzer_set_quiet() to apply the saved preference. */ +#if IS_ENABLED(CONFIG_ZEPHCORE_UI_BUZZER) + buzzer_set_quiet(companion_mesh.prefs.buzzer_quiet); + ui_set_buzzer_quiet(companion_mesh.prefs.buzzer_quiet); + LOG_INF("buzzer: quiet=%s (from prefs)", + companion_mesh.prefs.buzzer_quiet ? "true" : "false"); + ui_play_startup_chime(); +#endif + + /* Restore GPS state from persisted prefs. + * GPS hardware is powered at boot (bootloader/pull-up). + * First, ensure power state matches prefs (powers off if disabled). + * Then, if prefs say enabled, start the GPS state machine. */ + if (gps_is_available()) { + /* Explicitly set power state at boot (handles disabled case) */ + gps_ensure_power_state(companion_mesh.prefs.gps_enabled); + + if (companion_mesh.prefs.gps_enabled) { + LOG_INF("GPS: Restoring enabled state from prefs"); + gps_enable(true); + } + } + + /* Apply RX boost setting from prefs (overrides radio default if user changed it) */ + lora_radio.setRxBoost(companion_mesh.prefs.rx_boost != 0); + + /* Initialize mesh event object */ + k_event_init(&mesh_events); + + /* Initialize UI mesh actions module (pass mesh objects for deferred actions) */ + ui_mesh_actions_init(&mesh_events, MESH_EVENT_UI_ACTION, + &companion_mesh, &data_store, + &lora_radio, &zephyr_board, &rtc_clock); +#endif + + /* Initialize BLE adapter (registers auth callbacks) */ + zephcore_ble_init(&ble_cbs); + + /* + * USB CDC ACM - only enabled when logging is enabled (debug builds). + * Production builds (CONFIG_LOG=n) skip USB entirely, saving ~2-5mA. + */ +#if IS_ENABLED(CONFIG_LOG) + zephcore_usb_companion_init(&mesh_events, &rx_process_work, MESH_EVENT_BLE_RX, + &zephyr_board); +#endif + + if (bt_enable(bt_ready) != 0) { + LOG_ERR("bt_enable failed"); + } + + /* + * FULLY EVENT-DRIVEN architecture: main thread runs mesh event loop. + * Mesh loop wakes IMMEDIATELY on events - NO POLLING for TX completion! + * + * Event sources: + * - MESH_EVENT_LORA_RX: LoRa packet received (from RX async callback) + * - MESH_EVENT_LORA_TX_DONE: LoRa TX complete (from TX poll work -> callback) + * - MESH_EVENT_BLE_RX: BLE frame received (from NUS write) + * - MESH_EVENT_HOUSEKEEPING: Periodic maintenance (noise floor, etc.) + * + * Other processing still uses work queues: + * - BLE TX: write_frame -> tx_drain_work (event-driven via notify callback) + * - Contact iteration: contact_iter_work (on TX queue space) + * + * TX completion is now event-driven: radio polls at 5ms intervals internally + * and fires callback when done. Much better than 100ms main loop polling! + */ +#ifdef ZEPHCORE_LORA + mesh_event_loop(); /* Never returns */ +#else + for (;;) { + k_sleep(K_FOREVER); + } +#endif + + return 0; +} diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp new file mode 100644 index 0000000..cff1141 --- /dev/null +++ b/zephcore/src/main_repeater.cpp @@ -0,0 +1,375 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore - Repeater (USB CLI, Event-Driven) + * + * This is the main entry point for the repeater role. + * Repeaters use USB serial CLI for configuration (no BLE). + */ + +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(zephcore_repeater_main, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); + +#include +#include +#include +#include +#include +#include +#include "oled_power.h" + +/* USB CDC with 1200 baud touch detection (when not using auto-init) */ +#if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) +#include +#endif + +#include +#include +#include +#include + +/* Radio + mesh includes (shared header selects LR1110 or SX126x) */ +#include + +/* LED configuration */ +#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) +#define LED0_NODE DT_ALIAS(led0) +static const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(LED0_NODE, gpios); +#endif +#if DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios) +#define LED1_NODE DT_ALIAS(led1) +static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); +#endif + +/* USB CLI configuration */ +#define USB_RING_BUF_SIZE 512 +#define CLI_LINE_BUF_SIZE 256 + +/* + * Event-driven mesh loop - replaces 50ms polling with true event signaling. + * Events are signaled from ISR/callbacks, mesh loop wakes immediately. + */ +#define MESH_EVENT_LORA_RX BIT(0) /* LoRa packet received */ +#define MESH_EVENT_LORA_TX_DONE BIT(1) /* LoRa TX complete */ +#define MESH_EVENT_CLI_RX BIT(2) /* CLI command received */ +#define MESH_EVENT_HOUSEKEEPING BIT(3) /* Periodic housekeeping (noise floor, etc.) */ +#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_HOUSEKEEPING) + +/* Housekeeping interval - infrequent to preserve power savings */ +#define HOUSEKEEPING_INTERVAL_MS CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS + +/* Event object for mesh loop */ +static struct k_event mesh_events; + +/* USB CDC state */ +static const struct device *usb_dev; +static uint8_t usb_ring_buf_data[USB_RING_BUF_SIZE]; +static struct ring_buf usb_ring_buf; +static char cli_line_buf[CLI_LINE_BUF_SIZE]; +static char cli_reply_buf[256]; +static uint16_t cli_line_idx; + +/* Work items for event-driven processing */ +static void cli_rx_work_fn(struct k_work *work); +static void housekeeping_timer_fn(struct k_timer *timer); +K_WORK_DEFINE(cli_rx_work, cli_rx_work_fn); + +/* Housekeeping timer for periodic tasks (noise floor calibration, etc.) */ +K_TIMER_DEFINE(housekeeping_timer, housekeeping_timer_fn, NULL); + +/* Forward declarations */ +#ifdef ZEPHCORE_LORA +static RepeaterMesh *repeater_mesh_ptr; +#endif + +/* Print string to USB serial */ +static void cli_print(const char *str) +{ + if (!usb_dev) return; + while (*str) { + uart_poll_out(usb_dev, *str++); + } +} + +/* USB CDC UART interrupt callback */ +static void cli_uart_isr(const struct device *dev, void *user_data) +{ + ARG_UNUSED(user_data); + + while (uart_irq_update(dev) && uart_irq_is_pending(dev)) { + if (uart_irq_rx_ready(dev)) { + uint8_t buf[64]; + int recv_len = uart_fifo_read(dev, buf, sizeof(buf)); + if (recv_len > 0) { + ring_buf_put(&usb_ring_buf, buf, recv_len); + k_work_submit(&cli_rx_work); + } + } + } +} + +/* CLI RX work - processes line-based CLI commands + * Matches Arduino behavior: echo each char, then " -> reply" on enter + */ +static void cli_rx_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + uint8_t byte; + + while (ring_buf_get(&usb_ring_buf, &byte, 1) == 1) { + /* Process command on \r OR \n (support echo from Linux) */ + if (byte == '\r' || byte == '\n') { + if (cli_line_idx > 0) { + cli_line_buf[cli_line_idx] = '\0'; + + /* Debug: log received command */ + LOG_INF("CLI cmd len=%d: %.40s%s", cli_line_idx, + cli_line_buf, cli_line_idx > 40 ? "..." : ""); + + /* Process CLI command */ +#ifdef ZEPHCORE_LORA + if (repeater_mesh_ptr) { + cli_reply_buf[0] = '\0'; + repeater_mesh_ptr->handleCommand(0, cli_line_buf, cli_reply_buf); + if (cli_reply_buf[0] != '\0') { + /* Arduino format: newline, then " -> reply" */ + cli_print("\r\n -> "); + cli_print(cli_reply_buf); + } + } +#endif + cli_line_idx = 0; + } + /* New line for next command */ + cli_print("\r\n"); + } else if (byte == 0x7F || byte == 0x08) { + /* Backspace - echo backspace sequence */ + if (cli_line_idx > 0) { + cli_line_idx--; + if (usb_dev) { + uart_poll_out(usb_dev, '\b'); + uart_poll_out(usb_dev, ' '); + uart_poll_out(usb_dev, '\b'); + } + } + } else if (cli_line_idx < sizeof(cli_line_buf) - 1) { + /* Echo character back (like Arduino) */ + if (usb_dev) { + uart_poll_out(usb_dev, byte); + } + cli_line_buf[cli_line_idx++] = (char)byte; + } + } +} + +/* Housekeeping timer callback - signals event to wake mesh loop periodically */ +static void housekeeping_timer_fn(struct k_timer *timer) +{ + ARG_UNUSED(timer); + k_event_post(&mesh_events, MESH_EVENT_HOUSEKEEPING); +} + +#ifdef ZEPHCORE_LORA +/* LoRa RX callback - called from ISR context when packet received */ +static void lora_rx_callback(void *user_data) +{ + ARG_UNUSED(user_data); + k_event_post(&mesh_events, MESH_EVENT_LORA_RX); +} + +/* LoRa TX complete callback */ +static void lora_tx_done_callback(void *user_data) +{ + ARG_UNUSED(user_data); + k_event_post(&mesh_events, MESH_EVENT_LORA_TX_DONE); +} +#endif + +/* Global instances */ +static mesh::ZephyrRTCClock rtc_clock; +static RepeaterDataStore data_store; + +#ifdef ZEPHCORE_LORA +static mesh::ZephyrBoard zephyr_board; + +/* Radio prefs — initialized with defaults in main(), updated after mesh.begin() + * loads persisted prefs. Passed to radio adapter at static construction time. */ +static NodePrefs radio_prefs; + +#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) +/* LR1110 via Zephyr LoRa driver */ +static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); +static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board, &radio_prefs); +#else +/* SX126x via Zephyr LoRa driver */ +static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); +static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board, &radio_prefs); +#endif + +static mesh::ZephyrMillisecondClock ms_clock; +static mesh::ZephyrRNG zephyr_rng; +static mesh::SimpleMeshTables mesh_tables; + +/* RepeaterMesh requires: board, radio, ms_clock, rng, rtc, tables */ +static RepeaterMesh repeater_mesh(zephyr_board, lora_radio, ms_clock, zephyr_rng, rtc_clock, mesh_tables); +#endif + +/* Repeater event loop */ +static void repeater_event_loop(void) +{ + LOG_INF("starting event-driven loop"); + + /* Print startup banner (no prompt - Arduino style) */ + cli_print("\r\n=== ZephCore Repeater ===\r\n"); + + /* Start housekeeping timer for periodic maintenance tasks */ + k_timer_start(&housekeeping_timer, K_MSEC(HOUSEKEEPING_INTERVAL_MS), + K_MSEC(HOUSEKEEPING_INTERVAL_MS)); + + for (;;) { + /* Wait for any mesh event - blocks until signaled */ + uint32_t events = k_event_wait(&mesh_events, MESH_EVENT_ALL, false, K_FOREVER); + k_event_clear(&mesh_events, events); + +#ifdef ZEPHCORE_LORA + if (repeater_mesh_ptr) { + repeater_mesh_ptr->loop(); + } +#endif + } +} + +int main(void) +{ + /* Initialize radio prefs with safe defaults before anything else */ + initNodePrefs(&radio_prefs); + strcpy(radio_prefs.node_name, "Repeater"); + + /* Wait for USB CDC to enumerate before any logging */ + k_sleep(K_MSEC(2000)); + LOG_INF("=== ZephCore Repeater starting ==="); + + /* Configure LEDs */ +#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) + if (gpio_is_ready_dt(&led0)) { + gpio_pin_configure_dt(&led0, GPIO_OUTPUT_INACTIVE); + } +#endif +#if DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios) + if (gpio_is_ready_dt(&led1)) { + gpio_pin_configure_dt(&led1, GPIO_OUTPUT_INACTIVE); + } +#endif + + /* Initialize repeater data store */ + if (!data_store.begin()) { + LOG_ERR("RepeaterDataStore init failed"); + } + + /* Initialize sensor manager */ + sensor_manager_init(); + + /* Set GPS to repeater mode: power off now, wake every 48h for time sync only. + * This prevents GPS from draining power on boards that have it (e.g., Wio Tracker). */ + if (gps_is_available()) { + gps_set_repeater_mode(true); + } + + /* Put OLED display to sleep */ + oled_sleep(); + + /* Log environment sensor availability */ + if (env_sensors_available()) { + LOG_INF("Environment sensors available"); + } + +#ifdef ZEPHCORE_LORA + repeater_mesh_ptr = &repeater_mesh; + + /* Initialize mesh event object BEFORE begin() — radio callbacks post events */ + k_event_init(&mesh_events); + + /* Set LoRa callbacks for event-driven packet processing */ + lora_radio.setRxCallback(lora_rx_callback, nullptr); + lora_radio.setTxDoneCallback(lora_tx_done_callback, nullptr); + + /* Load or generate identity BEFORE begin() */ + mesh::LocalIdentity self_identity; + if (!data_store.loadIdentity(self_identity)) { + LOG_INF("No identity found, generating new keypair..."); + self_identity = mesh::LocalIdentity(&zephyr_rng); + /* Ensure pub_key[0] is not reserved (0x00 or 0xFF) */ + int count = 0; + while (count < 10 && (self_identity.pub_key[0] == 0x00 || self_identity.pub_key[0] == 0xFF)) { + self_identity = mesh::LocalIdentity(&zephyr_rng); + count++; + } + data_store.saveIdentity(self_identity); + LOG_INF("New identity saved"); + } + repeater_mesh.self_id = self_identity; + + /* Log repeater ID (first 8 bytes of public key) */ + LOG_INF("Repeater ID: %02x%02x%02x%02x%02x%02x%02x%02x...", + self_identity.pub_key[0], self_identity.pub_key[1], + self_identity.pub_key[2], self_identity.pub_key[3], + self_identity.pub_key[4], self_identity.pub_key[5], + self_identity.pub_key[6], self_identity.pub_key[7]); + + /* Start mesh with data store - this loads prefs, ACL, regions */ + repeater_mesh.begin(&data_store); + + /* Generate default node name from hardware device ID if not set */ + NodePrefs* prefs = repeater_mesh.getNodePrefs(); + if (strlen(prefs->node_name) == 0 || strcmp(prefs->node_name, "Repeater") == 0) { + uint8_t dev_id[8]; + ssize_t id_len = hwinfo_get_device_id(dev_id, sizeof(dev_id)); + if (id_len >= 4) { + snprintf(prefs->node_name, sizeof(prefs->node_name), + "Repeater-%02X%02X%02X%02X", dev_id[0], dev_id[1], dev_id[2], dev_id[3]); + } + } + + /* Apply RX boost setting from prefs (overrides radio default if user changed it) */ + lora_radio.setRxBoost(prefs->rx_boost != 0); + + /* Send initial zero-hop advertisement after boot */ + LOG_INF("Sending initial advertisement..."); + repeater_mesh.sendSelfAdvertisement(500, false); /* 500ms delay, zero-hop */ +#endif + + /* Initialize USB CDC for CLI with 1200 baud touch detection */ +#if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) + zephcore_usbd_init(); +#endif + usb_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart); + if (device_is_ready(usb_dev)) { + LOG_INF("USB CDC device ready: %s", usb_dev->name); + ring_buf_init(&usb_ring_buf, sizeof(usb_ring_buf_data), usb_ring_buf_data); + + /* Set up UART interrupt callback */ + uart_irq_callback_set(usb_dev, cli_uart_isr); + uart_irq_rx_enable(usb_dev); + } else { + LOG_ERR("USB CDC device not ready"); + usb_dev = NULL; + } + + /* + * Event-driven architecture: main thread runs repeater event loop. + * No BLE - all configuration via USB serial CLI. + */ +#ifdef ZEPHCORE_LORA + repeater_event_loop(); /* Never returns */ +#else + for (;;) { + k_sleep(K_FOREVER); + } +#endif + + return 0; +} diff --git a/zephcore/tools/formatter/CMakeLists.txt b/zephcore/tools/formatter/CMakeLists.txt new file mode 100644 index 0000000..83279ce --- /dev/null +++ b/zephcore/tools/formatter/CMakeLists.txt @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: Apache-2.0 +# ZephCore Flash Formatter — erases all filesystem partitions, reboots to UF2 DFU. +# +# Build: west build -b rak4631/nrf52840 zephcore/tools/formatter --pristine +# Flash: drag-drop build/zephyr/zephyr.uf2 onto UF2 drive + +cmake_minimum_required(VERSION 3.20.0) + +# Reuse the main project's custom board definitions (overlays for partitions) +list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..) + +# Extract base board name (strip qualifier like /nrf52840) +string(REPLACE "/" ";" BOARD_PARTS ${BOARD}) +list(GET BOARD_PARTS 0 BOARD_BASE) + +# Find board.overlay — prefer formatter-specific overlay (minimal, no sensors/GPS), +# fall back to main board overlay (full hardware definitions). +# Formatter overlays live in tools/formatter/boards//board.overlay +file(GLOB_RECURSE FMT_OVERLAY_CANDIDATES + "${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/board.overlay" + "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay") +if(FMT_OVERLAY_CANDIDATES) + list(GET FMT_OVERLAY_CANDIDATES 0 FORMATTER_BOARD_OVERLAY) +else() + # Fall back to main board overlay (partitions + full hardware) + file(GLOB_RECURSE BOARD_OVERLAY_CANDIDATES + "${CMAKE_CURRENT_SOURCE_DIR}/../../boards/*/${BOARD_BASE}/board.overlay") + if(BOARD_OVERLAY_CANDIDATES) + list(GET BOARD_OVERLAY_CANDIDATES 0 FORMATTER_BOARD_OVERLAY) + endif() +endif() +if(FORMATTER_BOARD_OVERLAY) + if(EXTRA_DTC_OVERLAY_FILE) + set(EXTRA_DTC_OVERLAY_FILE "${EXTRA_DTC_OVERLAY_FILE};${FORMATTER_BOARD_OVERLAY}" CACHE STRING "" FORCE) + else() + set(EXTRA_DTC_OVERLAY_FILE "${FORMATTER_BOARD_OVERLAY}" CACHE STRING "" FORCE) + endif() +endif() + +# Find formatter-specific board.conf (e.g., QSPI enablement) +# These live in tools/formatter/boards//board.conf +file(GLOB_RECURSE FMT_BOARD_CONF_CANDIDATES + "${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/board.conf" + "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.conf") +if(FMT_BOARD_CONF_CANDIDATES) + list(GET FMT_BOARD_CONF_CANDIDATES 0 FMT_BOARD_CONF) + if(EXTRA_CONF_FILE) + set(EXTRA_CONF_FILE "${EXTRA_CONF_FILE};${FMT_BOARD_CONF}" CACHE STRING "" FORCE) + else() + set(EXTRA_CONF_FILE "${FMT_BOARD_CONF}" CACHE STRING "" FORCE) + endif() +endif() + +message(STATUS "Formatter board overlay: ${FORMATTER_BOARD_OVERLAY}") +message(STATUS "Formatter board conf: ${FMT_BOARD_CONF}") + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(zephcore_formatter) + +target_sources(app PRIVATE src/main.c) diff --git a/zephcore/tools/formatter/boards/rak4631/board.overlay b/zephcore/tools/formatter/boards/rak4631/board.overlay new file mode 100644 index 0000000..005c41b --- /dev/null +++ b/zephcore/tools/formatter/boards/rak4631/board.overlay @@ -0,0 +1,61 @@ +/* + * RAK4631 formatter overlay — flash partitions + USB console only. + * Strips all sensor/GPS/radio nodes from the main board overlay + * to avoid driver init hangs in the minimal formatter environment. + */ + +/delete-node/ &boot_partition; +/delete-node/ &slot0_partition; +/delete-node/ &slot1_partition; +/delete-node/ &storage_partition; + +/ { + chosen { + zephyr,code-partition = &code_partition; + zephyr,console = &cdc_acm_uart; + zephyr,shell-uart = &cdc_acm_uart; + zephyr,settings-partition = &storage_partition; + }; +}; + +&zephyr_udc0 { + cdc_acm_uart: cdc_acm_uart { + compatible = "zephyr,cdc-acm-uart"; + }; +}; + +/* Disable QSPI — RAK19003 Mini Base has no QSPI flash */ +&qspi { + status = "disabled"; +}; + +&flash0 { + partitions { + boot_partition: partition@0 { + label = "softdevice"; + read-only; + reg = <0x00000000 0x00026000>; + }; + + code_partition: partition@26000 { + label = "Application"; + reg = <0x00026000 0x000AE000>; + }; + + storage_partition: partition@d4000 { + label = "storage"; + reg = <0x000D4000 0x00002000>; + }; + + lfs_partition: partition@d6000 { + label = "lfs"; + reg = <0x000D6000 0x0001E000>; + }; + + uf2_partition: partition@f4000 { + label = "UF2"; + read-only; + reg = <0x000F4000 0x0000C000>; + }; + }; +}; diff --git a/zephcore/tools/formatter/boards/wio_tracker_l1/board.conf b/zephcore/tools/formatter/boards/wio_tracker_l1/board.conf new file mode 100644 index 0000000..ae8d9c7 --- /dev/null +++ b/zephcore/tools/formatter/boards/wio_tracker_l1/board.conf @@ -0,0 +1,3 @@ +# Wio Tracker L1 — QSPI flash present (P25Q16H 2MB) +CONFIG_NORDIC_QSPI_NOR=y +CONFIG_NORDIC_QSPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096 diff --git a/zephcore/tools/formatter/prj.conf b/zephcore/tools/formatter/prj.conf new file mode 100644 index 0000000..594d36c --- /dev/null +++ b/zephcore/tools/formatter/prj.conf @@ -0,0 +1,42 @@ +# ZephCore Flash Formatter — minimal config +# Board-specific QSPI enablement comes from board.conf + +# Flash access +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y + +# Use DTS code_partition for linker origin (required for bootloader boards) +CONFIG_USE_DT_CODE_PARTITION=y + +# UF2 output +CONFIG_BUILD_OUTPUT_UF2=y + +# USB CDC console (new stack — must match main app) +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_SERIAL=y +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y +CONFIG_UART_LINE_CTRL=y +CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=y +CONFIG_CDC_ACM_SERIAL_ENABLE_AT_BOOT=y + +# No logging, no BLE, no LoRa — just erase and reboot +CONFIG_LOG=n +CONFIG_ASSERT=n + +# GPIO for LED feedback +CONFIG_GPIO=y + +# Reboot API +CONFIG_REBOOT=y + +# Don't mount filesystems — we're erasing them +CONFIG_FILE_SYSTEM=n + +# Disable peripherals auto-enabled by board overlay DTS nodes. +# Without this, GNSS/I2C/sensor drivers init and hang on boards +# where GPS or sensors aren't physically connected. +CONFIG_GNSS=n +CONFIG_I2C=n +CONFIG_SENSOR=n +CONFIG_SPI=n diff --git a/zephcore/tools/formatter/src/main.c b/zephcore/tools/formatter/src/main.c new file mode 100644 index 0000000..041199c --- /dev/null +++ b/zephcore/tools/formatter/src/main.c @@ -0,0 +1,128 @@ +/* + * ZephCore Universal Flash Formatter + * + * Erases all filesystem partitions (NVS + LittleFS + QSPI if present) + * and reboots into Adafruit UF2 DFU mode for clean firmware installation. + * + * Partition addresses come from the board's devicetree overlay — no + * hardcoded addresses, works for both s140 v6.1.1 and v7.3.0 boards. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_SOC_SERIES_NRF52X) || defined(CONFIG_SOC_SERIES_NRF52) +#include +#endif + +/* Adafruit UF2 bootloader magic — enter mass storage DFU mode */ +#define BOOTLOADER_DFU_UF2_MAGIC 0x57 + +/* ── LED feedback (optional) ─────────────────────────────────── */ + +#if DT_NODE_EXISTS(DT_ALIAS(led0)) +#define HAS_LED 1 +static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios); +static void led_init(void) { gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); } +static void led_on(void) { gpio_pin_set_dt(&led, 1); } +static void led_off(void) { gpio_pin_set_dt(&led, 0); } +#else +#define HAS_LED 0 +static void led_init(void) {} +static void led_on(void) {} +static void led_off(void) {} +#endif + +/* ── Partition erase helper ──────────────────────────────────── */ + +static int erase_partition(uint8_t id, const char *name) +{ + const struct flash_area *fa; + int rc; + + rc = flash_area_open(id, &fa); + if (rc) { + printk(" %s: open failed (rc %d) — skipped\n", name, rc); + return rc; + } + + printk(" %s: erasing 0x%lx - 0x%lx (%u KB)...", + name, + (unsigned long)fa->fa_off, + (unsigned long)(fa->fa_off + fa->fa_size), + (unsigned)(fa->fa_size / 1024)); + + rc = flash_area_erase(fa, 0, fa->fa_size); + if (rc) { + printk(" FAILED (rc %d)\n", rc); + } else { + printk(" OK\n"); + } + + flash_area_close(fa); + return rc; +} + +/* ── Main ────────────────────────────────────────────────────── */ + +int main(void) +{ + int errors = 0; + + /* Brief delay for USB CDC to enumerate */ + k_msleep(2000); + + printk("\n"); + printk("=== ZephCore Flash Formatter ===\n"); + printk("\n"); + + led_init(); + led_on(); + + /* ── Erase internal NVS partition (BLE bonds + settings) ── */ + if (erase_partition(FIXED_PARTITION_ID(storage_partition), "NVS (storage)")) { + errors++; + } + + /* ── Erase internal LittleFS partition (identity, contacts, channels) ── */ + if (erase_partition(FIXED_PARTITION_ID(lfs_partition), "LittleFS (lfs)")) { + errors++; + } + + /* ── Erase external QSPI flash (if present in devicetree) ── */ +#if DT_NODE_EXISTS(DT_NODELABEL(qspi_storage_partition)) + if (erase_partition(FIXED_PARTITION_ID(qspi_storage_partition), "QSPI external")) { + errors++; + } +#else + printk(" QSPI: not present on this board — skipped\n"); +#endif + + led_off(); + + /* ── Summary ── */ + printk("\n"); + if (errors) { + printk("Completed with %d error(s).\n", errors); + } else { + printk("All partitions erased successfully.\n"); + } + + printk("Rebooting into UF2 DFU mode...\n"); + printk("You can now drag-drop your new firmware UF2.\n"); + k_msleep(500); + + /* ── Reboot to UF2 bootloader ── */ +#if defined(CONFIG_SOC_SERIES_NRF52X) || defined(CONFIG_SOC_SERIES_NRF52) + nrf_power_gpregret_set(NRF_POWER, 0, BOOTLOADER_DFU_UF2_MAGIC); +#endif + sys_reboot(SYS_REBOOT_COLD); + + return 0; /* never reached */ +} diff --git a/zephcore/west.yml b/zephcore/west.yml new file mode 100644 index 0000000..0242a6b --- /dev/null +++ b/zephcore/west.yml @@ -0,0 +1,13 @@ +manifest: + remotes: + - name: zephyrproject-rtos + url-base: https://github.com/zephyrproject-rtos + + projects: + - name: zephyr + remote: zephyrproject-rtos + revision: bf9007f3819bd324c6f9d93fc6f9aef3474be12c + import: true + + self: + path: zephcore